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

[karaf] branch KARAF-5376-overrides_v2 updated (e730633 -> c6bca02)

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

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


 discard e730633  [KARAF-5468] Cleaning up AssemblyMojo, Profiles and profile Builder
 discard 5e1c44d  [KARAF-5376] Polish LocationPattern and FeaturePattern helpers for blacklist/override matching
 discard 196b8ff  [KARAF-5478] Provide org.apache.karaf.util.Version class
 discard 07bbb7d  Revert "[KARAF-5416] Remove support for ext and endorsed libraries for Java 9 compatibility"
 discard e7ad6ea  [KARAF-5376] Review BuilderTest
 discard ddeb7db  [KARAF-5376] Include new Features JAXB model info in feature:* commands
 discard 4662072  [KARAF-5376] Moving overrides and blacklist mechanisms to "features processor" service
     add 7ad0da3  [KARAF-5427] Shell reflection / redirection should require admin role by default
     add 3d57887  [KARAF-5475] Add a ClientPrincipal containing the connection method / remote ip
     add 851c582  [KARAF-5475] Send an event to EventAdmin after having executed a command in the shell and include the result / exception
     add 26a948b  [KARAF-5475] Add EventAdmin support to JMX
     add 6ff0873  [KARAF-5475] Create an audit bundle that logs to file / tcp / udp / jul
     add 1a24059  [KARAF-5475] Fix tests to be timezone independent
     add 46e8b90  [KARAF-5446] Fragment bundles are not resolved properly when installing/restarting the container
     add b186ba3  Fix performance issue generating service metadata, change logging
     add 92b505c  [KARAF-5494] Merge branch 'master' of https://github.com/mhus/karaf into mhus-master
     add 75f345c  [KARAF-5475] Fix tests to be timezone independent
     add 4bb3dc2  [KARAF-3674] Improve Karaf scheduler documentation
     add 63b0a6c  [KARAF-5475] Remove "log_" prefix in subtype, use a synchronous bundle listener to obtain the subject correctly
     add 1d70817  [KARAF-5475] Ability to use custom layouts using fragments
     add e57cb70  Fix gogo webconsole plugin
     add b46deee  Use a StringBuilder instead of string concatenations
     add c89d5aa  [KARAF-5475] Disable the audit log LoginModule (superceeded by the event admin log)
     add a0ee11e  Partial revert of KARAF-5416 to keep libraries on java 1.8 specific runtimes
     add a23ee51  [KARAF-5475] Fix timezone string computation
     add 915894b  KARAF-5496 - NPEs in SyncopeLoginModule if "version" is not specified
     add 5dcbf17  Merge pull request #398 from coheigea/KARAF-5496
     add af1219e  KARAF-5498 - SyncopeLoginModule parses roles instead of groups for Syncope 2.0.x  - Adding a configuration option to fall back to parsing the roles if required.
     add 02a4d42  Merge pull request #399 from coheigea/KARAF-5498
     add 256efd6  [KARAF-5488] Upgrade to Felix Framework 5.6.10
     add beb0284  Merge pull request #394 from jbonofre/KARAF-5488
     add 0edb20b  [KARAF-5506]ensure we also check the ACL for alias cmds before auto-completer
     add 552a173  Exclude MavenTest from surefire execution to give us time to investigate
     add cb78db8  Merge pull request #403 from jbonofre/EXCLUDE_MAVENTEST
     add 768f13b  [KARAF-5495] Support Syncope 2.x in SyncopeBackingEngine
     add 14b87bc  Merge pull request #402 from jbonofre/KARAF-5495
     add be495c1  [KARAF-5489] Upgrade to commons-io 2.6
     add 15c3977  Merge pull request #395 from jbonofre/KARAF-5489
     add 1bae9e2  [KARAF-5490] Upgrade to JNA 4.5.0
     add 1214050  Merge pull request #396 from jbonofre/KARAF-5490
     add 874e409  [KARAF-5491] Upgrade to commons-compress 1.15
     add 51c90f6  Merge pull request #397 from jbonofre/KARAF-5491
     add 5583c32  [KARAF-5307] Add SchedulerMBean
     add 24c299e  Merge pull request #401 from jbonofre/KARAF-5307
     add f8849fc  [KARAF-5506]ensure we also check the ACL for alias cmds before auto-completer-more change
     add 8ee2ebb  [KARAF-5506]use ThreadLocal for the visible service cache so that different shell for different users/roles won't interfere each other
     add ccf7657  Karaf 5412 (#404)
     add b137b36  [KARAF-5475] Remove additional space
     new 4533176  [KARAF-5376] Moving overrides and blacklist mechanisms to "features processor" service
     new 0d302a5  [KARAF-5376] Include new Features JAXB model info in feature:* commands
     new 032a71a  [KARAF-5376] Review BuilderTest
     new 8a668a4  [KARAF-5478] Provide org.apache.karaf.util.Version class
     new 37e211b  [KARAF-5376] Polish LocationPattern and FeaturePattern helpers for blacklist/override matching
     new dd1990c  [KARAF-5468] Cleaning up AssemblyMojo, Profiles and profile Builder
     new 59cb985  [KARAF-5468] Add comments to generated target/assembly/etc/profile.cfg
     new b7c55c0  [KARAF-5376] Using "features processor" in profile builder (overrides, blacklist)
     new abd345a  [KARAF-5376] Generate/merge features processor configuration from external and Maven config, improve logging in karaf-maven-plugin:assembly
     new a51b572  [KARAF-5468] Adjust JavaVersion enum: supportsEndorsedAndExtLibraries()
     new c6bca02  [KARAF-5376] Integrate features processor with Deployer and SubsystemResolver

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (e730633)
            \
             N -- N -- N   refs/heads/KARAF-5376-overrides_v2 (c6bca02)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 11 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 assemblies/features/base/pom.xml                   |  10 -
 .../resources/etc/org.ops4j.pax.logging.cfg        |  10 +-
 assemblies/features/framework/pom.xml              |   5 -
 assemblies/features/standard/pom.xml               |   3 -
 .../features/standard/src/main/feature/feature.xml | 119 +++-
 {config => audit}/pom.xml                          |  28 +-
 .../java/org/apache/karaf/audit/Activator.java     | 539 +++++++++++++++++
 .../main/java/org/apache/karaf/audit/Event.java    |  46 +-
 .../java/org/apache/karaf/audit/EventLayout.java   |  23 +-
 .../java/org/apache/karaf/audit/EventLogger.java   |  12 +-
 .../apache/karaf/audit/layout/AbstractLayout.java  | 198 +++++++
 .../org/apache/karaf/audit/layout/GelfLayout.java  |  89 +++
 .../apache/karaf/audit/layout/Rfc3164Layout.java   |  87 +++
 .../apache/karaf/audit/layout/Rfc5424Layout.java   |  88 +++
 .../apache/karaf/audit/layout/SimpleLayout.java    |  69 +++
 .../apache/karaf/audit/logger/FileEventLogger.java | 291 +++++++++
 .../apache/karaf/audit/logger/JulEventLogger.java  |  61 ++
 .../apache/karaf/audit/logger/TcpEventLogger.java  |  67 +++
 .../apache/karaf/audit/logger/UdpEventLogger.java  |  84 +++
 .../java/org/apache/karaf/audit/util/Buffer.java   | 306 ++++++++++
 .../apache/karaf/audit/util/FastDateFormat.java    | 177 ++++++
 .../org/apache/karaf/audit/util/NumberOutput.java  | 516 ++++++++++++++++
 .../test/java/org/apache/karaf/audit/MapEvent.java |  51 +-
 .../test/java/org/apache/karaf/audit/TestPerf.java | 152 +++++
 .../apache/karaf/audit/logger/EventLoggerTest.java | 266 +++++++++
 .../karaf/audit/util/FastDateFormatTest.java       |  49 ++
 exception/NOTICE                                   |  67 ---
 exception/pom.xml                                  |  38 --
 exception/src/main/java/java/lang/Exception.java   | 194 ------
 .../karaf/features/command/InfoFeatureCommand.java |   2 +-
 features/core/pom.xml                              |   1 +
 .../java/org/apache/karaf/features/BundleInfo.java |  21 +-
 .../org/apache/karaf/features/FeaturePattern.java  |  32 +-
 .../org/apache/karaf/features/FeaturesService.java |  64 +-
 .../java/org/apache/karaf/features/Library.java    |  10 +
 .../org/apache/karaf/features/LocationPattern.java |  11 +-
 .../karaf/features/internal/model/Bundle.java      |   8 +-
 .../karaf/features/internal/model/Conditional.java |   2 +
 .../karaf/features/internal/model/Features.java    |  16 +-
 .../model/processing/FeaturesProcessing.java       |  67 ++-
 .../karaf/features/internal/osgi/Activator.java    |   4 +-
 .../karaf/features/internal/region/Subsystem.java  | 242 ++++++--
 .../internal/region/SubsystemResolveContext.java   |  12 +-
 .../internal/region/SubsystemResolver.java         |  98 +++-
 .../internal/region/SubsystemResolverCallback.java |  16 +-
 .../region/SubsystemResolverResolution.java        |  82 +++
 .../internal/region/SubsystemResolverResult.java   |  78 +++
 .../internal/resolver/FeatureResource.java         |  32 +-
 .../features/internal/resolver/ResolverUtil.java   |   5 +
 .../features/internal/resolver/ResourceUtils.java  |  11 +
 .../karaf/features/internal/service/Blacklist.java |  37 ++
 .../internal/service/BundleInstallSupport.java     |  13 +-
 .../internal/service/BundleInstallSupportImpl.java |   6 +-
 .../karaf/features/internal/service/Deployer.java  | 275 ++++++---
 .../service/FeaturesProcessingSerializer.java      | 159 +++++
 .../internal/service/FeaturesProcessor.java        |  11 +-
 .../internal/service/FeaturesProcessorImpl.java    | 131 +++--
 .../internal/service/FeaturesServiceConfig.java    |  33 ++
 .../internal/service/FeaturesServiceImpl.java      |  20 +-
 .../karaf/features/internal/service/Overrides.java |  32 +
 .../internal/service/RepositoryCacheImpl.java      |   2 +-
 .../features/internal/service/RepositoryImpl.java  |   1 +
 .../karaf/features/internal/service/State.java     |  19 +-
 .../karaf/features/internal/util/MapUtils.java     |  29 +
 .../service/feature-processing-comments.properties |  27 +
 .../features/karaf-features-processing-1.0.0.xsd   |  22 +-
 .../internal/region/FeaturesDependenciesTest.java  |  13 +-
 .../features/internal/region/SubsystemTest.java    | 174 ++++--
 .../features/internal/service/DeployerTest.java    |  48 +-
 .../internal/service/FeaturesProcessorTest.java    |  55 +-
 .../internal/region/{data7/a.mf => data1/d.mf}     |   2 +-
 .../internal/region/{data6 => data10}/a.mf         |   0
 .../internal/region/{data6 => data10}/b.mf         |   0
 .../internal/region/{data3 => data10}/features.xml |   3 +-
 .../features/internal/region/data3/{b.mf => c.mf}  |   2 +-
 .../features/extension/StoredWiringResolver.java   |   4 +-
 itests/pom.xml                                     |  16 +-
 itests/src/test/filtered-resources/etc/feature.xml |  16 +-
 jaas/blueprint/jasypt/pom.xml                      |  22 +-
 .../karaf/jaas/boot/principal/ClientPrincipal.java |  54 ++
 .../apache/karaf/jaas/modules/impl/Activator.java  |   2 +-
 .../jaas/modules/syncope/SyncopeBackingEngine.java |  97 ++-
 .../syncope/SyncopeBackingEngineFactory.java       |   3 +-
 .../jaas/modules/syncope/SyncopeLoginModule.java   |  30 +-
 .../modules/syncope/SyncopeLoginModuleTest.java    |  18 +-
 management/server/pom.xml                          |   8 +-
 .../apache/karaf/management/JaasAuthenticator.java |   7 +
 .../karaf/management/KarafMBeanServerGuard.java    |  40 +-
 .../karaf/management/internal/Activator.java       |  31 +
 .../management/internal/EventAdminLogger.java      |  12 +-
 .../management/internal/EventAdminLoggerImpl.java  |  60 ++
 .../internal/EventAdminMBeanServerWrapper.java     | 648 +++++++++++++++++++++
 .../internal/MBeanInvocationHandler.java           |   3 +
 .../main/asciidoc/user-guide/os-integration.adoc   |   6 +-
 manual/src/main/asciidoc/user-guide/scheduler.adoc |  36 +-
 pom.xml                                            |  47 +-
 profile/pom.xml                                    |  16 +-
 .../org/apache/karaf/profile/ProfileConstants.java |   5 +
 .../karaf/profile/assembly/ArtifactInstaller.java  |  51 +-
 .../profile/assembly/AssemblyDeployCallback.java   |  59 +-
 .../org/apache/karaf/profile/assembly/Builder.java | 503 +++++++++++-----
 .../karaf/profile/assembly/ConfigInstaller.java    |   8 +-
 .../karaf/profile/impl/ProfileBuilderImpl.java     | 187 +++++-
 .../apache/karaf/profile/impl/ProfilesTest.java    |  15 +-
 .../apache/karaf/profile/impl/PropertiesTest.java  |  21 +-
 .../org/apache/karaf/scheduler/SchedulerMBean.java |  20 +-
 .../org/apache/karaf/scheduler/core/Activator.java |   4 +
 .../karaf/scheduler/core/SchedulerMBeanImpl.java   |  91 +++
 service/guard/pom.xml                              |  33 +-
 .../felix/eventadmin/impl/Configuration.java       |   5 +-
 .../impl/adapter/BundleEventAdapter.java           | 127 ++++
 .../shell/impl/console/CommandsCompleter.java      |  31 +-
 .../shell/impl/console/ConsoleSessionImpl.java     |   6 +-
 .../karaf/shell/impl/console/RegistryImpl.java     |   7 +
 .../shell/impl/console/SessionFactoryImpl.java     |   3 +
 .../impl/console/osgi/EventAdminListener.java      |  32 +-
 .../impl/console/osgi/LocalConsoleManager.java     |   2 +
 .../impl/console/osgi/secured/AliasCommand.java}   |  31 +-
 .../osgi/secured/SecuredSessionFactoryImpl.java    | 117 +++-
 .../karaf/shell/ssh/KarafJaasAuthenticator.java    |   2 +
 tooling/karaf-maven-plugin/pom.xml                 |   1 -
 .../org/apache/karaf/tooling/AssemblyMojo.java     |  17 +
 .../java/org/apache/karaf/tooling/VerifyMojo.java  |  68 +--
 .../tooling/tracker/GenerateServiceMetadata.java   |  22 +-
 .../karaf/util/xml/IndentingXMLEventWriter.java    | 144 +++++
 .../internal/servlet/JaasSecurityProvider.java     |   9 +-
 .../org/apache/karaf/webconsole/gogo/Terminal.java | 108 ++--
 .../karaf/wrapper/internal/unix/karaf-wrapper.conf |   7 +-
 .../wrapper/internal/windows/karaf-wrapper.conf    |   7 +-
 .../wrapper/internal/windows64/karaf-wrapper.conf  |   7 +-
 130 files changed, 7266 insertions(+), 1233 deletions(-)
 copy {config => audit}/pom.xml (80%)
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/Activator.java
 copy bundle/core/src/main/java/org/apache/karaf/bundle/core/BundleInfo.java => audit/src/main/java/org/apache/karaf/audit/Event.java (54%)
 copy scheduler/src/main/java/org/apache/karaf/scheduler/JobContext.java => audit/src/main/java/org/apache/karaf/audit/EventLayout.java (63%)
 copy features/core/src/main/java/org/apache/karaf/features/RegionDigraphPersistence.java => audit/src/main/java/org/apache/karaf/audit/EventLogger.java (78%)
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/layout/AbstractLayout.java
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/layout/GelfLayout.java
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/layout/Rfc3164Layout.java
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/layout/Rfc5424Layout.java
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/layout/SimpleLayout.java
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/logger/FileEventLogger.java
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/logger/JulEventLogger.java
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/logger/TcpEventLogger.java
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/logger/UdpEventLogger.java
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/util/Buffer.java
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/util/FastDateFormat.java
 create mode 100644 audit/src/main/java/org/apache/karaf/audit/util/NumberOutput.java
 copy log/src/main/java/org/apache/karaf/log/core/internal/LogMBeanImpl.java => audit/src/test/java/org/apache/karaf/audit/MapEvent.java (51%)
 create mode 100644 audit/src/test/java/org/apache/karaf/audit/TestPerf.java
 create mode 100644 audit/src/test/java/org/apache/karaf/audit/logger/EventLoggerTest.java
 create mode 100644 audit/src/test/java/org/apache/karaf/audit/util/FastDateFormatTest.java
 delete mode 100644 exception/NOTICE
 delete mode 100644 exception/pom.xml
 delete mode 100644 exception/src/main/java/java/lang/Exception.java
 copy shell/core/src/main/java/org/apache/karaf/shell/api/console/SignalListener.java => features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolverCallback.java (69%)
 create mode 100644 features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolverResolution.java
 create mode 100644 features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolverResult.java
 create mode 100644 features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessingSerializer.java
 create mode 100644 features/core/src/main/resources/org/apache/karaf/features/internal/service/feature-processing-comments.properties
 copy features/core/src/test/resources/org/apache/karaf/features/internal/region/{data7/a.mf => data1/d.mf} (82%)
 copy features/core/src/test/resources/org/apache/karaf/features/internal/region/{data6 => data10}/a.mf (100%)
 copy features/core/src/test/resources/org/apache/karaf/features/internal/region/{data6 => data10}/b.mf (100%)
 copy features/core/src/test/resources/org/apache/karaf/features/internal/region/{data3 => data10}/features.xml (96%)
 copy features/core/src/test/resources/org/apache/karaf/features/internal/region/data3/{b.mf => c.mf} (71%)
 create mode 100644 jaas/boot/src/main/java/org/apache/karaf/jaas/boot/principal/ClientPrincipal.java
 copy profile/src/main/java/org/apache/karaf/profile/LockHandle.java => management/server/src/main/java/org/apache/karaf/management/internal/EventAdminLogger.java (81%)
 create mode 100644 management/server/src/main/java/org/apache/karaf/management/internal/EventAdminLoggerImpl.java
 create mode 100644 management/server/src/main/java/org/apache/karaf/management/internal/EventAdminMBeanServerWrapper.java
 copy shell/core/src/test/java/org/apache/karaf/shell/support/ansi/AnsiSplitterTest.java => profile/src/test/java/org/apache/karaf/profile/impl/PropertiesTest.java (56%)
 copy package/src/main/java/org/apache/karaf/packages/core/PackagesMBean.java => scheduler/src/main/java/org/apache/karaf/scheduler/SchedulerMBean.java (74%)
 create mode 100644 scheduler/src/main/java/org/apache/karaf/scheduler/core/SchedulerMBeanImpl.java
 create mode 100644 services/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/BundleEventAdapter.java
 copy shell/{console/src/main/java/org/apache/karaf/shell/console/CommandSessionHolder.java => core/src/main/java/org/apache/karaf/shell/impl/console/osgi/secured/AliasCommand.java} (62%)
 create mode 100755 util/src/main/java/org/apache/karaf/util/xml/IndentingXMLEventWriter.java

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

[karaf] 02/11: [KARAF-5376] Include new Features JAXB model info in feature:* commands

Posted by gg...@apache.org.
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 0d302a5b00ae110a53846055033232ed8134c8a3
Author: Grzegorz Grzybek <gr...@gmail.com>
AuthorDate: Mon Nov 6 13:24:22 2017 +0100

    [KARAF-5376] Include new Features JAXB model info in feature:* commands
---
 .../karaf/features/command/InfoFeatureCommand.java |  3 +++
 .../features/command/ListFeaturesCommand.java      | 17 +++++++++++++++-
 .../karaf/features/command/RepoListCommand.java    | 23 +++++++++++++++++-----
 .../internal/service/RepositoryCacheImpl.java      |  2 ++
 .../features/internal/service/RepositoryImpl.java  |  6 ++++++
 5 files changed, 45 insertions(+), 6 deletions(-)

diff --git a/features/command/src/main/java/org/apache/karaf/features/command/InfoFeatureCommand.java b/features/command/src/main/java/org/apache/karaf/features/command/InfoFeatureCommand.java
index 1ecc0a0..7c6a775 100644
--- a/features/command/src/main/java/org/apache/karaf/features/command/InfoFeatureCommand.java
+++ b/features/command/src/main/java/org/apache/karaf/features/command/InfoFeatureCommand.java
@@ -209,6 +209,9 @@ public class InfoFeatureCommand extends FeaturesCommandSupport {
                 if(startLevel > 0) {
                     sb.append(" start-level=").append(startLevel);
                 }
+                if (featureBundle.isOverriden()) {
+                    sb.append(" (overriden from " + featureBundle.getOriginalLocation() + ")");
+                }
                 System.out.println(sb.toString());
             }
         }
diff --git a/features/command/src/main/java/org/apache/karaf/features/command/ListFeaturesCommand.java b/features/command/src/main/java/org/apache/karaf/features/command/ListFeaturesCommand.java
index 6bf9f9c..9e17153 100644
--- a/features/command/src/main/java/org/apache/karaf/features/command/ListFeaturesCommand.java
+++ b/features/command/src/main/java/org/apache/karaf/features/command/ListFeaturesCommand.java
@@ -27,6 +27,7 @@ import org.apache.karaf.features.Repository;
 import org.apache.karaf.shell.api.action.Command;
 import org.apache.karaf.shell.api.action.Option;
 import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.support.table.Row;
 import org.apache.karaf.shell.support.table.ShellTable;
 
 @Command(scope = "feature", name = "list", description = "Lists all existing features available from the defined repositories.")
@@ -42,6 +43,9 @@ public class ListFeaturesCommand extends FeaturesCommandSupport {
     @Option(name = "-s", aliases = {"--show-hidden"}, description = "Display hidden features", required = false, multiValued = false)
     boolean showHidden;
 
+    @Option(name = "-b", aliases = {"--show-blacklisted"}, description = "Display blacklisted features", required = false, multiValued = false)
+    boolean showBlacklisted;
+
     @Option(name = "-o", aliases = {"--ordered"}, description = "Display a list using alphabetical order ", required = false, multiValued = false)
     boolean ordered;
 
@@ -58,6 +62,9 @@ public class ListFeaturesCommand extends FeaturesCommandSupport {
         table.column("State");
         table.column("Repository");
         table.column("Description").maxSize(50);
+        if (showBlacklisted) {
+            table.column("Blacklisted");
+        }
         table.emptyTableText(onlyInstalled ? "No features installed" : "No features available");
 
         List<Repository> repos = Arrays.asList(featuresService.listRepositories());
@@ -75,17 +82,25 @@ public class ListFeaturesCommand extends FeaturesCommandSupport {
                     // Filter out not installed features if we only want to see the installed ones
                     continue;
                 }
+                if (!showBlacklisted && f.isBlacklisted()) {
+                    // Filter out blacklisted
+                    continue;
+                }
                 if (!showHidden && f.isHidden()) {
                     // Filter out hidden feature if not asked to display those
                     continue;
                 }
-                table.addRow().addContent(
+                Row row = table.addRow();
+                row.addContent(
                         f.getName(),
                         f.getVersion(),
                         featuresService.isRequired(f) ? "x" : "",
                         featuresService.getState(f.getId()),
                         r.getName(),
                         f.getDescription());
+                if (showBlacklisted) {
+                    row.addContent(f.isBlacklisted() ? "yes" : "no");
+                }
                 if (isInstalledViaDeployDir(r.getName())) {
                     needsLegend = true;
                 }
diff --git a/features/command/src/main/java/org/apache/karaf/features/command/RepoListCommand.java b/features/command/src/main/java/org/apache/karaf/features/command/RepoListCommand.java
index 50f3b95..a91182f 100644
--- a/features/command/src/main/java/org/apache/karaf/features/command/RepoListCommand.java
+++ b/features/command/src/main/java/org/apache/karaf/features/command/RepoListCommand.java
@@ -25,6 +25,7 @@ import org.apache.karaf.shell.api.action.Command;
 import org.apache.karaf.shell.api.action.Option;
 import org.apache.karaf.shell.api.action.lifecycle.Service;
 import org.apache.karaf.shell.support.MultiException;
+import org.apache.karaf.shell.support.table.Row;
 import org.apache.karaf.shell.support.table.ShellTable;
 
 @Command(scope = "feature", name = "repo-list", description = "Displays a list of all defined repositories.")
@@ -34,9 +35,12 @@ public class RepoListCommand extends FeaturesCommandSupport {
     @Option(name="-r", description="Reload all feature urls", required = false, multiValued = false)
     boolean reload;
 
+    @Option(name = "-b", aliases = { " --show-blacklisted" }, description = "Also display blacklisted repositories", required = false, multiValued = false)
+    boolean showBlacklisted = false;
+
     @Option(name = "--no-format", description = "Disable table rendered output", required = false, multiValued = false)
     boolean noFormat;
-    
+
     protected void doExecute(FeaturesService featuresService) throws Exception {
         if (reload) {
             reloadAllRepos(featuresService);
@@ -45,15 +49,24 @@ public class RepoListCommand extends FeaturesCommandSupport {
         ShellTable table = new ShellTable();
         table.column("Repository");
         table.column("URL");
+        if (showBlacklisted) {
+            table.column("Blacklisted");
+        }
         table.emptyTableText("No repositories available");
 
         Repository[] repos = featuresService.listRepositories();
-     	for (Repository repo : repos) {
+        for (Repository repo : repos) {
             if (repo != null) {
-     	        table.addRow().addContent(repo.getName(), repo.getURI().toString()); 
+                if (showBlacklisted || !repo.isBlacklisted()) {
+                    Row row = table.addRow();
+                    row.addContent(repo.getName(), repo.getURI().toString());
+                    if (showBlacklisted) {
+                        row.addContent(repo.isBlacklisted() ? "yes" : "no");
+                    }
+                }
             }
-     	}
-     	table.print(System.out, !noFormat);
+        }
+        table.print(System.out, !noFormat);
     }
 
     private void reloadAllRepos(FeaturesService featuresService) throws Exception {
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCacheImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCacheImpl.java
index d33b39d..9874a20 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCacheImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCacheImpl.java
@@ -46,7 +46,9 @@ public class RepositoryCacheImpl implements RepositoryCache {
     public Repository create(URI uri, boolean validate) {
         RepositoryImpl repository = new RepositoryImpl(uri, validate);
         if (featuresProcessor != null) {
+            // maybe it could be done better - first we have to set if entire repo is blacklisted
             repository.setBlacklisted(featuresProcessor.isRepositoryBlacklisted(uri));
+            // processing features will take the above flag into account to blacklist (if needed) the features
             repository.processFeatures(featuresProcessor);
         }
         return repository;
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 31aadaf..3d76c23 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
@@ -107,6 +107,12 @@ public class RepositoryImpl implements Repository {
      */
     public void processFeatures(FeaturesProcessor processor) {
         processor.process(features);
+        if (blacklisted) {
+            // all features of blacklisted repository are blacklisted too
+            for (org.apache.karaf.features.internal.model.Feature feature : features.getFeature()) {
+                feature.setBlacklisted(true);
+            }
+        }
     }
 
     static class InterruptibleInputStream extends FilterInputStream {

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

[karaf] 04/11: [KARAF-5478] Provide org.apache.karaf.util.Version class

Posted by gg...@apache.org.
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 8a668a45df7ad3312dcb7a4342e0a512bd01a68c
Author: Grzegorz Grzybek <gr...@gmail.com>
AuthorDate: Fri Nov 10 13:23:11 2017 +0100

    [KARAF-5478] Provide org.apache.karaf.util.Version class
---
 util/pom.xml                                       |  9 +++++
 .../org/apache/karaf/util}/versions.properties     |  0
 .../main/java/org/apache/karaf/util/Version.java   | 43 ++++++++++++++++++++++
 3 files changed, 52 insertions(+)

diff --git a/util/pom.xml b/util/pom.xml
index 672b39a..fb12697 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -68,4 +68,13 @@
         <appendedResourcesDirectory>${basedir}/../etc/appended-resources</appendedResourcesDirectory>
     </properties>
 
+    <build>
+        <resources>
+            <resource>
+                <directory>${project.basedir}/src/main/filtered-resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+    </build>
+
 </project>
diff --git a/tooling/karaf-maven-plugin/src/main/filtered-resources/org/apache/karaf/tooling/versions.properties b/util/src/main/filtered-resources/org/apache/karaf/util/versions.properties
similarity index 100%
rename from tooling/karaf-maven-plugin/src/main/filtered-resources/org/apache/karaf/tooling/versions.properties
rename to util/src/main/filtered-resources/org/apache/karaf/util/versions.properties
diff --git a/util/src/main/java/org/apache/karaf/util/Version.java b/util/src/main/java/org/apache/karaf/util/Version.java
new file mode 100644
index 0000000..73d3476
--- /dev/null
+++ b/util/src/main/java/org/apache/karaf/util/Version.java
@@ -0,0 +1,43 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public class Version {
+
+    private static String version = "unknown";
+
+    static {
+        Properties versions = new Properties();
+        try (InputStream is = Version.class.getResourceAsStream("versions.properties")) {
+            versions.load(is);
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+        version = versions.getProperty("karaf-version");
+    }
+
+    public static String karafVersion() {
+        return version;
+    }
+
+}

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

[karaf] 08/11: [KARAF-5376] Using "features processor" in profile builder (overrides, blacklist)

Posted by gg...@apache.org.
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 b7c55c0605b39a66d41003625df28f9794d7a0fe
Author: Grzegorz Grzybek <gr...@gmail.com>
AuthorDate: Wed Nov 22 12:08:11 2017 +0100

    [KARAF-5376] Using "features processor" in profile builder (overrides, blacklist)
---
 features/core/pom.xml                              |   1 +
 .../org/apache/karaf/features/FeaturePattern.java  |  15 +-
 .../org/apache/karaf/features/LocationPattern.java |   2 +-
 .../model/processing/FeaturesProcessing.java       |  27 +-
 .../karaf/features/internal/service/Blacklist.java |  25 ++
 .../karaf/features/internal/service/Deployer.java  |  12 +
 .../service/FeaturesProcessingSerializer.java      | 159 ++++++++++
 .../internal/service/FeaturesProcessor.java        |   4 +-
 .../internal/service/FeaturesProcessorImpl.java    | 102 ++++---
 .../internal/service/RepositoryCacheImpl.java      |   2 +-
 .../service/feature-processing-comments.properties |  27 ++
 .../features/karaf-features-processing-1.0.0.xsd   |  22 +-
 .../internal/service/FeaturesProcessorTest.java    |  42 ++-
 .../karaf/profile/assembly/ArtifactInstaller.java  |   9 +-
 .../profile/assembly/AssemblyDeployCallback.java   |  32 +-
 .../org/apache/karaf/profile/assembly/Builder.java | 334 ++++++++++++++-------
 .../org/apache/karaf/tooling/AssemblyMojo.java     |   7 +
 .../java/org/apache/karaf/tooling/VerifyMojo.java  |  14 +-
 .../karaf/util/xml/IndentingXMLEventWriter.java    | 144 +++++++++
 19 files changed, 769 insertions(+), 211 deletions(-)

diff --git a/features/core/pom.xml b/features/core/pom.xml
index 028de45..e5eaa85 100644
--- a/features/core/pom.xml
+++ b/features/core/pom.xml
@@ -156,6 +156,7 @@
                             org.apache.karaf.util.collections,
                             org.apache.karaf.util.json,
                             org.apache.karaf.util.maven,
+                            org.apache.karaf.util.xml,
                             org.eclipse.equinox.internal.region.*;-split-package:=merge-first,
                             org.apache.felix.resolver.*,
                         </Private-Package>
diff --git a/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java b/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java
index 06db75a..107993b 100644
--- a/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java
+++ b/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java
@@ -41,6 +41,7 @@ public class FeaturePattern {
     public static final String RANGE = "range";
 
     private String originalId;
+    private String nameString;
     private Pattern namePattern;
     private String versionString;
     private Version version;
@@ -51,16 +52,16 @@ public class FeaturePattern {
             throw new IllegalArgumentException("Feature ID to match should not be null");
         }
         originalId = featureId;
-        String name = originalId;
-        if (name.indexOf("/") > 0) {
-            name = originalId.substring(0, originalId.indexOf("/"));
+        nameString = originalId;
+        if (originalId.indexOf("/") > 0) {
+            nameString = originalId.substring(0, originalId.indexOf("/"));
             versionString = originalId.substring(originalId.indexOf("/") + 1);
-        } else if (name.contains(";")) {
+        } else if (originalId.contains(";")) {
             Clause[] c = org.apache.felix.utils.manifest.Parser.parseClauses(new String[] { originalId });
-            name = c[0].getName();
+            nameString = c[0].getName();
             versionString = c[0].getAttribute(RANGE);
         }
-        namePattern = LocationPattern.toRegExp(name);
+        namePattern = LocationPattern.toRegExp(nameString);
 
         if (versionString != null && versionString.length() >= 1) {
             try {
@@ -80,7 +81,7 @@ public class FeaturePattern {
     }
 
     /**
-     * Returns <code>if this feature pattern</code> matches given feature/version
+     * Returns <code>true</code> if this feature pattern matches given feature/version
      * @param featureName
      * @param featureVersion
      * @return
diff --git a/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java b/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
index 20390ca..428746f 100644
--- a/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
+++ b/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
@@ -113,7 +113,7 @@ public class LocationPattern {
      * @param value
      * @return
      */
-    static Pattern toRegExp(String value) {
+    public static Pattern toRegExp(String value) {
         // TODO: escape all RegExp special chars that are valid path characters, only convert '*' into '.*'
         return Pattern.compile(value
                 .replaceAll("\\.", "\\\\\\.")
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java
index c2a0765..8a3c7b6 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java
@@ -20,6 +20,7 @@ package org.apache.karaf.features.internal.model.processing;
 
 import java.net.MalformedURLException;
 import java.net.URI;
+import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
@@ -37,6 +38,7 @@ 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.FeaturePattern;
 import org.apache.karaf.features.internal.service.Blacklist;
 import org.apache.karaf.features.LocationPattern;
 import org.osgi.framework.Version;
@@ -183,17 +185,32 @@ public class FeaturesProcessing {
         // 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.add(bl + ";" + Blacklist.BLACKLIST_TYPE + "=" + Blacklist.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() + "\""))
+                .map(bf -> bf.getName() + ";" + Blacklist.BLACKLIST_TYPE + "=" + Blacklist.TYPE_FEATURE + (bf.getVersion() == null ? "" : ";" + FeaturePattern.RANGE + "=\"" + bf.getVersion() + "\""))
                 .collect(Collectors.toList()));
+        // blacklisted repositories
+        for (String bl : this.getBlacklistedRepositories()) {
+            blacklisted.add(bl + ";" + Blacklist.BLACKLIST_TYPE + "=" + Blacklist.TYPE_REPOSITORY);
+        }
 
         this.blacklist = new Blacklist(blacklisted);
         this.blacklist.merge(blacklist);
 
         // etc/overrides.properties (mvn: URIs)
+        bundleReplacements.getOverrideBundles().addAll(parseOverridesClauses(overrides));
+    }
+
+    /**
+     * Changes overrides list (old format) into a list of {@link BundleReplacements.OverrideBundle} definitions.
+     * @param overrides
+     * @return
+     */
+    public static Collection<? extends BundleReplacements.OverrideBundle> parseOverridesClauses(Set<String> overrides) {
+        List<BundleReplacements.OverrideBundle> result = new LinkedList<>();
+
         for (Clause clause : Parser.parseClauses(overrides.toArray(new String[overrides.size()]))) {
             // name of the clause will become a bundle replacement
             String mvnURI = clause.getName();
@@ -210,12 +227,14 @@ public class FeaturesProcessing {
                 override.setOriginalUri(originalUri);
                 try {
                     override.compile();
-                    bundleReplacements.getOverrideBundles().add(override);
+                    result.add(override);
                 } catch (MalformedURLException e) {
                     LOG.warn("Can't parse override URL location pattern: " + originalUri + ". Ignoring.");
                 }
             }
         }
+
+        return result;
     }
 
     /**
@@ -225,7 +244,7 @@ public class FeaturesProcessing {
      * @param range
      * @return
      */
-    private String calculateOverridenURI(String replacement, String range) {
+    private static 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
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 046c4be..dcc1a20 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
@@ -40,6 +40,7 @@ import org.slf4j.LoggerFactory;
 /**
  * Helper class to deal with blacklisted features and bundles. It doesn't process JAXB model at all - it only
  * provides information about repository/feature/bundle being blacklisted.
+ * The task of actual blacklisting (altering JAXB model) is performed in {@link FeaturesProcessor}
  */
 public class Blacklist {
 
@@ -218,4 +219,28 @@ public class Blacklist {
     public void blacklist(Features featuresModel) {
     }
 
+    /**
+     * Directly add {@link LocationPattern} as blacklisted features XML repository URI
+     * @param locationPattern
+     */
+    public void blacklistRepository(LocationPattern locationPattern) {
+        repositoryBlacklist.add(locationPattern);
+    }
+
+    /**
+     * Directly add {@link FeaturePattern} as blacklisted feature ID
+     * @param featurePattern
+     */
+    public void blacklistFeature(FeaturePattern featurePattern) {
+        featureBlacklist.add(featurePattern);
+    }
+
+    /**
+     * Directly add {@link LocationPattern} as blacklisted bundle URI
+     * @param locationPattern
+     */
+    public void blacklistBundle(LocationPattern locationPattern) {
+        bundleBlacklist.add(locationPattern);
+    }
+
 }
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 61244b6..571e923 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
@@ -200,6 +200,18 @@ public class Deployer {
         public Map<String, Map<String, FeatureState>> stateChanges;
         public EnumSet<FeaturesService.Option> options;
         public String outputFile;
+
+        public static DeploymentRequest defaultDeploymentRequest() {
+            DeploymentRequest request = new DeploymentRequest();
+            request.bundleUpdateRange = FeaturesService.DEFAULT_BUNDLE_UPDATE_RANGE;
+            request.featureResolutionRange = FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE;
+            request.serviceRequirements = FeaturesService.SERVICE_REQUIREMENTS_DEFAULT;
+            request.overrides = new HashSet<>();
+            request.requirements = new HashMap<>();
+            request.stateChanges = new HashMap<>();
+            request.options = EnumSet.noneOf(FeaturesService.Option.class);
+            return request;
+        }
     }
 
     static class Deployment {
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessingSerializer.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessingSerializer.java
new file mode 100644
index 0000000..4a72fe3
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessingSerializer.java
@@ -0,0 +1,159 @@
+/*
+ * 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.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLEventFactory;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLEventWriter;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.events.XMLEvent;
+import javax.xml.transform.stream.StreamResult;
+
+import org.apache.karaf.features.internal.model.processing.FeaturesProcessing;
+import org.apache.karaf.features.internal.model.processing.ObjectFactory;
+import org.apache.karaf.util.xml.IndentingXMLEventWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A class to help serialize {@link org.apache.karaf.features.internal.model.processing.FeaturesProcessing} model
+ * but with added template comments for main sections of <code>org.apache.karaf.features.xml</code> file.
+ */
+public class FeaturesProcessingSerializer {
+
+    public static Logger LOG = LoggerFactory.getLogger(FeaturesProcessingSerializer.class);
+
+    private JAXBContext FEATURES_PROCESSING_CONTEXT;
+
+    public FeaturesProcessingSerializer() {
+        try {
+            FEATURES_PROCESSING_CONTEXT = JAXBContext.newInstance(ObjectFactory.class);
+        } catch (JAXBException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Reads {@link FeaturesProcessing features processing model} from input stream
+     * @param stream
+     * @return
+     */
+    public FeaturesProcessing read(InputStream stream) throws JAXBException {
+        Unmarshaller unmarshaller = FEATURES_PROCESSING_CONTEXT.createUnmarshaller();
+        return (FeaturesProcessing) unmarshaller.unmarshal(stream);
+    }
+
+    /**
+     * Writes the model to output stream and adds comments for main sections.
+     * @param model
+     * @param output
+     */
+    public void write(FeaturesProcessing model, OutputStream output) {
+        try {
+            // JAXB model as stream which is next parsed as XMLEvents
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            Marshaller marshaller = FEATURES_PROCESSING_CONTEXT.createMarshaller();
+            marshaller.marshal(model, new StreamResult(baos));
+
+            Map<String, Boolean> emptyElements = new HashMap<>();
+            emptyElements.put("blacklistedRepositories", model.getBlacklistedRepositories().size() == 0);
+            emptyElements.put("blacklistedFeatures", model.getBlacklistedFeatures().size() == 0);
+            emptyElements.put("blacklistedBundles", model.getBlacklistedBundles().size() == 0);
+            emptyElements.put("overrideBundleDependency", model.getOverrideBundleDependency().getRepositories().size()
+                    + model.getOverrideBundleDependency().getFeatures().size()
+                    + model.getOverrideBundleDependency().getBundles().size() == 0);
+            emptyElements.put("bundleReplacements", model.getBundleReplacements().getOverrideBundles().size() == 0);
+            emptyElements.put("featureReplacements", model.getFeatureReplacements().getReplacements().size() == 0);
+
+            // A mix of direct write and stream of XML events. It's not easy (without knowing StAX impl) to
+            // output self closed tags for example.
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, "UTF-8"));
+            writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n");
+            writer.write("    Configuration generated by Karaf Assembly Builder\n");
+            writer.write("-->\n");
+            writer.flush();
+
+            Properties props = new Properties();
+            props.load(getClass().getResourceAsStream("feature-processing-comments.properties"));
+
+            XMLEventReader xmlEventReader = XMLInputFactory.newFactory().createXMLEventReader(new ByteArrayInputStream(baos.toByteArray()));
+            XMLEventWriter xmlEventWriter = new IndentingXMLEventWriter(XMLOutputFactory.newFactory().createXMLEventWriter(writer), "    ");
+            XMLEventFactory evFactory = XMLEventFactory.newFactory();
+            int depth = 0;
+            boolean skipClose = false;
+            while (xmlEventReader.hasNext()) {
+                XMLEvent ev = xmlEventReader.nextEvent();
+                int type = ev.getEventType();
+                if (type != XMLEvent.START_DOCUMENT && type != XMLEvent.END_DOCUMENT) {
+                    if (type == XMLEvent.START_ELEMENT) {
+                        skipClose = false;
+                        depth++;
+                        if (depth == 2) {
+                            String tag = ev.asStartElement().getName().getLocalPart();
+                            String comment = props.getProperty(tag);
+                            xmlEventWriter.add(evFactory.createCharacters("\n    "));
+                            xmlEventWriter.add(evFactory.createComment(" " + comment + " "));
+                            if (emptyElements.get(tag) != null && emptyElements.get(tag)) {
+                                skipClose = true;
+                                writer.write("    <" + tag + " />\n");
+                            }
+                        }
+                    } else if (type == XMLEvent.END_ELEMENT) {
+                        skipClose = false;
+                        depth--;
+                        if (depth == 1) {
+                            String tag = ev.asEndElement().getName().getLocalPart();
+                            String comment = props.getProperty(tag);
+                            if (emptyElements.get(tag) != null && emptyElements.get(tag)) {
+                                skipClose = true;
+                            }
+                        }
+                    }
+                    if (type == XMLEvent.END_ELEMENT && depth == 0) {
+                        xmlEventWriter.add(evFactory.createCharacters("\n"));
+                    }
+                    if (!skipClose) {
+                        xmlEventWriter.add(ev);
+                    }
+                    if (type == XMLEvent.START_ELEMENT && depth == 1) {
+                        xmlEventWriter.add(evFactory.createCharacters("\n"));
+                    }
+                }
+            }
+            writer.flush();
+        } catch (Exception e) {
+            LOG.warn(e.getMessage(), e);
+        }
+    }
+
+}
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
index 28e805d..7a74fae 100644
--- 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
@@ -18,8 +18,6 @@
  */
 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;
 
@@ -33,7 +31,7 @@ public interface FeaturesProcessor {
      * @param uri
      * @return
      */
-    boolean isRepositoryBlacklisted(URI uri);
+    boolean isRepositoryBlacklisted(String uri);
 
     /**
      * Processes original {@link Features JAXB model of 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
index c14c559..c6ed5a6 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessorImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessorImpl.java
@@ -20,13 +20,10 @@ package org.apache.karaf.features.internal.service;
 
 import java.io.FileNotFoundException;
 import java.io.InputStream;
-import java.net.URI;
+import java.io.OutputStream;
 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.LocationPattern;
@@ -36,7 +33,6 @@ 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.FeaturesProcessing;
-import org.apache.karaf.features.internal.model.processing.ObjectFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -52,41 +48,22 @@ import org.slf4j.LoggerFactory;
 public class FeaturesProcessorImpl implements FeaturesProcessor {
 
     public static Logger LOG = LoggerFactory.getLogger(FeaturesProcessorImpl.class);
-    private static final JAXBContext FEATURES_PROCESSING_CONTEXT;
 
+    private static FeaturesProcessingSerializer serializer = new FeaturesProcessingSerializer();
     private FeaturesProcessing processing = new FeaturesProcessing();
 
-    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
+     * <p>Creates instance of features processor using 1 external URI, additional {@link Blacklist} instance
+     * and additional set of override clauses.</p>
+     * @param featureModificationsURI
+     * @param blacklistDefinitions
+     * @param overrides
      */
-    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);
-
+    public FeaturesProcessorImpl(String featureModificationsURI, Blacklist blacklistDefinitions, Set<String> overrides) {
         if (featureModificationsURI != null) {
             try {
                 try (InputStream stream = new URL(featureModificationsURI).openStream()) {
-                    Unmarshaller unmarshaller = FEATURES_PROCESSING_CONTEXT.createUnmarshaller();
-                    processing = (FeaturesProcessing) unmarshaller.unmarshal(stream);
+                    processing = serializer.read(stream);
                 }
             } catch (FileNotFoundException e) {
                 LOG.warn("Can't find feature processing file (" + featureModificationsURI + ")");
@@ -95,13 +72,49 @@ public class FeaturesProcessorImpl implements FeaturesProcessor {
             }
         }
 
-        processing.postUnmarshall(blacklist, overrides);
+        processing.postUnmarshall(blacklistDefinitions, overrides);
+    }
+
+    /**
+     * <p>Creates instance of features processor using 3 external (optional) URIs.</p>
+     * @param featureModificationsURI
+     * @param blacklistedURI
+     * @param overridesURI
+     */
+    public FeaturesProcessorImpl(String featureModificationsURI, String blacklistedURI, String overridesURI) {
+        this(featureModificationsURI, new Blacklist(blacklistedURI), Overrides.loadOverrides(overridesURI));
+    }
+
+    /**
+     * <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) {
+        this(configuration.featureModifications, configuration.blacklisted, configuration.overrides);
+    }
+
+    /**
+     * Writes model to output stream.
+     * @param output
+     */
+    public void writeInstructions(OutputStream output) {
+        serializer.write(processing, output);
     }
 
     public FeaturesProcessing getInstructions() {
         return processing;
     }
 
+    /**
+     * For the purpose of assembly builder, we can configure additional overrides that are read from profiles
+     * @param overrides
+     */
+    public void addOverrides(Set<String> overrides) {
+        processing.getBundleReplacements().getOverrideBundles()
+                .addAll(FeaturesProcessing.parseOverridesClauses(overrides));
+    }
+
     @Override
     public void process(Features features) {
         // blacklisting features
@@ -153,9 +166,9 @@ public class FeaturesProcessorImpl implements FeaturesProcessor {
     }
 
     @Override
-    public boolean isRepositoryBlacklisted(URI uri) {
+    public boolean isRepositoryBlacklisted(String uri) {
         for (LocationPattern lp : processing.getBlacklistedRepositoryLocationPatterns()) {
-            if (lp.matches(uri.toString())) {
+            if (lp.matches(uri)) {
                 return true;
             }
         }
@@ -181,4 +194,23 @@ public class FeaturesProcessorImpl implements FeaturesProcessor {
         return getInstructions().getBlacklist().isBundleBlacklisted(location);
     }
 
+    /**
+     * Checks whether the configuration in this processor contains any instructions (for bundles, repositories,
+     * overrides, ...)
+     * @return
+     */
+    public boolean hasInstructions() {
+        int count = 0;
+        count += getInstructions().getBlacklistedRepositories().size();
+        count += getInstructions().getBlacklistedFeatures().size();
+        count += getInstructions().getBlacklistedBundles().size();
+        count += getInstructions().getOverrideBundleDependency().getRepositories().size();
+        count += getInstructions().getOverrideBundleDependency().getFeatures().size();
+        count += getInstructions().getOverrideBundleDependency().getBundles().size();
+        count += getInstructions().getBundleReplacements().getOverrideBundles().size();
+        count += getInstructions().getFeatureReplacements().getReplacements().size();
+
+        return count > 0;
+    }
+
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCacheImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCacheImpl.java
index 9874a20..0848f55 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCacheImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCacheImpl.java
@@ -47,7 +47,7 @@ public class RepositoryCacheImpl implements RepositoryCache {
         RepositoryImpl repository = new RepositoryImpl(uri, validate);
         if (featuresProcessor != null) {
             // maybe it could be done better - first we have to set if entire repo is blacklisted
-            repository.setBlacklisted(featuresProcessor.isRepositoryBlacklisted(uri));
+            repository.setBlacklisted(featuresProcessor.isRepositoryBlacklisted(uri.toString()));
             // processing features will take the above flag into account to blacklist (if needed) the features
             repository.processFeatures(featuresProcessor);
         }
diff --git a/features/core/src/main/resources/org/apache/karaf/features/internal/service/feature-processing-comments.properties b/features/core/src/main/resources/org/apache/karaf/features/internal/service/feature-processing-comments.properties
new file mode 100644
index 0000000..29bb1c9
--- /dev/null
+++ b/features/core/src/main/resources/org/apache/karaf/features/internal/service/feature-processing-comments.properties
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+
+# comments to insert into serialized XML for features processing instructions
+
+blacklistedRepositories = A list of blacklisted features XML repository URIs - they can't be added later
+blacklistedFeatures = A list of blacklisted feature identifiers that can't be installed in Karaf and are not part of the distribution
+blacklistedBundles = A list of blacklisted bundle URIs that are not installed even if they are part of some features
+overrideBundleDependency = A list of repository URIs, feature identifiers and bundle URIs to override "dependency" flag
+bundleReplacements = A list of bundle URI replacements that allows changing external feature definitions
+featureReplacements = A list of feature replacements that allows changing external feature definitions
diff --git a/features/core/src/main/resources/org/apache/karaf/features/karaf-features-processing-1.0.0.xsd b/features/core/src/main/resources/org/apache/karaf/features/karaf-features-processing-1.0.0.xsd
index b82bf08..f25fabe 100644
--- a/features/core/src/main/resources/org/apache/karaf/features/karaf-features-processing-1.0.0.xsd
+++ b/features/core/src/main/resources/org/apache/karaf/features/karaf-features-processing-1.0.0.xsd
@@ -60,17 +60,12 @@ should be specified. In most abstract case, "mvn:*/*" describes all maven URIs.
 
     <xs:complexType name="blacklistedFeatures">
         <xs:annotation>
-            <xs:documentation><![CDATA[A list of feature names (and related OSGi clause attributes) that should be
-blacklisted.
+            <xs:documentation><![CDATA[A list of feature identifiers 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>
@@ -102,11 +97,8 @@ 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
+version ranges may be specified as maven versions, e.g., "mvn:group/artifact/[3,5)/type/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.
@@ -137,7 +129,8 @@ override this flag for any repository, feature or particular bundles.
     <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.
+Maven schemes allow parsing version/version ranges. "dependency" flag will overwrite dependency attribute of related
+features (in repository) and bundles.
 ]]></xs:documentation>
         </xs:annotation>
         <xs:attribute name="uri" type="xs:anyURI" />
@@ -145,6 +138,11 @@ Maven schemes allow parsing version/version ranges.
     </xs:complexType>
 
     <xs:complexType name="overrideFeatureDependency">
+        <xs:annotation>
+            <xs:documentation><![CDATA[After matching feature by name (may use "*" glob) and version (or range), we
+can overwrite "dependency" attribute on given feature
+]]></xs:documentation>
+        </xs:annotation>
         <xs:attribute name="name" type="xs:string" />
         <xs:attribute name="version" type="xs:string" />
         <xs:attribute name="dependency" type="xs:boolean" default="false" />
@@ -158,8 +156,6 @@ names match. With bundleReplacements element, its possible to do the same and mo
 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>
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
index ce96722..63cc100 100644
--- 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
@@ -25,9 +25,12 @@ 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.Bundle;
 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.apache.karaf.util.maven.Parser;
 import org.junit.Test;
 import org.slf4j.Logger;
@@ -120,7 +123,7 @@ public class FeaturesProcessorTest {
         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"));
+        assertFalse(blacklist.isFeatureBlacklisted("jclouds", "1"));
 
         assertTrue(blacklist.isBundleBlacklisted("mvn:org.spring/spring-infinity/42"));
         assertFalse(blacklist.isBundleBlacklisted("mvn:org.spring/spring-infinity/41"));
@@ -137,9 +140,9 @@ public class FeaturesProcessorTest {
         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]));
+        assertFalse(processor.isRepositoryBlacklisted(repo.getRepositories()[0].toString()));
+        assertTrue(processor.isRepositoryBlacklisted(repo.getRepositories()[1].toString()));
+        assertFalse(processor.isRepositoryBlacklisted(repo.getRepositories()[2].toString()));
     }
 
     @Test
@@ -197,4 +200,35 @@ public class FeaturesProcessorTest {
         assertFalse(f1.getConditional().get(0).getBundles().get(1).isOverriden());
     }
 
+    @Test
+    public void serializeWithComments() {
+        FeaturesProcessingSerializer serializer = new FeaturesProcessingSerializer();
+        FeaturesProcessing featuresProcessing = new FeaturesProcessing();
+        featuresProcessing.getBlacklistedRepositories().add("repository 1");
+        OverrideBundleDependency.OverrideDependency d1 = new OverrideBundleDependency.OverrideDependency();
+        d1.setDependency(true);
+        d1.setUri("uri 1");
+        featuresProcessing.getOverrideBundleDependency().getRepositories().add(d1);
+        OverrideBundleDependency.OverrideFeatureDependency d2 = new OverrideBundleDependency.OverrideFeatureDependency();
+        d2.setDependency(false);
+        d2.setName("n");
+        d2.setVersion("1.2.3");
+        featuresProcessing.getOverrideBundleDependency().getFeatures().add(d2);
+        BundleReplacements.OverrideBundle override = new BundleReplacements.OverrideBundle();
+        override.setOriginalUri("original");
+        override.setReplacement("replacement");
+        override.setMode(BundleReplacements.BundleOverrideMode.OSGI);
+        featuresProcessing.getBundleReplacements().getOverrideBundles().add(override);
+        FeatureReplacements.OverrideFeature of = new FeatureReplacements.OverrideFeature();
+        of.setMode(FeatureReplacements.FeatureOverrideMode.REPLACE);
+        org.apache.karaf.features.internal.model.Feature f = new org.apache.karaf.features.internal.model.Feature();
+        f.setName("f1");
+        Bundle b = new Bundle();
+        b.setLocation("location");
+        f.getBundle().add(b);
+        of.setFeature(f);
+        featuresProcessing.getFeatureReplacements().getReplacements().add(of);
+        serializer.write(featuresProcessing, System.out);
+    }
+
 }
diff --git a/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java b/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java
index c06ebf2..967a979 100644
--- a/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java
+++ b/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java
@@ -16,15 +16,12 @@
  */
 package org.apache.karaf.profile.assembly;
 
-import static org.apache.karaf.features.internal.download.impl.DownloadManagerHelper.stripUrl;
-
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
-import java.util.List;
 
 import org.apache.karaf.features.internal.download.Downloader;
 import org.apache.karaf.features.internal.service.Blacklist;
@@ -32,6 +29,8 @@ import org.apache.karaf.util.maven.Parser;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import static org.apache.karaf.features.internal.download.impl.DownloadManagerHelper.stripUrl;
+
 /**
  * Downloads a maven artifact and installs it into the given system directory.
  * The layout follows the conventions of a maven local repository.
@@ -43,10 +42,10 @@ public class ArtifactInstaller {
     private Downloader downloader;
     private Blacklist blacklist;
 
-    public ArtifactInstaller(Path systemDirectory, Downloader downloader, List<String> blacklisted) {
+    public ArtifactInstaller(Path systemDirectory, Downloader downloader, Blacklist blacklist) {
         this.systemDirectory = systemDirectory;
         this.downloader = downloader;
-        this.blacklist = new Blacklist(blacklisted);
+        this.blacklist = blacklist;
     }
     
     public void installArtifact(String location) throws Exception {
diff --git a/profile/src/main/java/org/apache/karaf/profile/assembly/AssemblyDeployCallback.java b/profile/src/main/java/org/apache/karaf/profile/assembly/AssemblyDeployCallback.java
index 2be1d6f..6e854dc 100644
--- a/profile/src/main/java/org/apache/karaf/profile/assembly/AssemblyDeployCallback.java
+++ b/profile/src/main/java/org/apache/karaf/profile/assembly/AssemblyDeployCallback.java
@@ -51,7 +51,6 @@ import org.apache.karaf.features.internal.util.MapUtils;
 import org.apache.karaf.util.maven.Parser;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleException;
-import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.framework.startlevel.BundleStartLevel;
 import org.osgi.framework.wiring.BundleRevision;
 import org.slf4j.Logger;
@@ -74,12 +73,11 @@ public class AssemblyDeployCallback extends StaticInstallSupport implements Depl
 
     private final Map<String, Bundle> bundles = new HashMap<>();
 
-
-    public AssemblyDeployCallback(DownloadManager manager, Builder builder, BundleRevision systemBundle, Collection<Features> repositories) throws Exception {
+    public AssemblyDeployCallback(DownloadManager manager, Builder builder, BundleRevision systemBundle, Collection<Features> repositories) {
         this.manager = manager;
         this.builder = builder;
-        this.featureBlacklist = new Blacklist(builder.getBlacklistedFeatures());
-        this.bundleBlacklist = new Blacklist(builder.getBlacklistedBundles());
+//        this.featureBlacklist = new Blacklist(builder.getBlacklistedFeatures());
+//        this.bundleBlacklist = new Blacklist(builder.getBlacklistedBundles());
         this.homeDirectory = builder.homeDirectory;
         this.etcDirectory = homeDirectory.resolve("etc");
         this.systemDirectory = homeDirectory.resolve("system");
@@ -122,11 +120,11 @@ public class AssemblyDeployCallback extends StaticInstallSupport implements Depl
     }
 
     @Override
-    public void persistResolveRequest(Deployer.DeploymentRequest request) throws IOException {
+    public void persistResolveRequest(Deployer.DeploymentRequest request) {
     }
 
     @Override
-    public void installConfigs(org.apache.karaf.features.Feature feature) throws IOException, InvalidSyntaxException {
+    public void installConfigs(org.apache.karaf.features.Feature feature) throws IOException {
         assertNotBlacklisted(feature);
         // Install
         Downloader downloader = manager.createDownloader();
@@ -195,11 +193,11 @@ public class AssemblyDeployCallback extends StaticInstallSupport implements Depl
     }
     
     private void assertNotBlacklisted(org.apache.karaf.features.Feature feature) {
-        if (featureBlacklist.isFeatureBlacklisted(feature.getName(), feature.getVersion())) {
-            if (builder.getBlacklistPolicy() == Builder.BlacklistPolicy.Fail) {
-                throw new RuntimeException("Feature " + feature.getId() + " is blacklisted");
-            }
-        }
+//        if (featureBlacklist.isFeatureBlacklisted(feature.getName(), feature.getVersion())) {
+//            if (builder.getBlacklistPolicy() == Builder.BlacklistPolicy.Fail) {
+//                throw new RuntimeException("Feature " + feature.getId() + " is blacklisted");
+//            }
+//        }
     }
 
     @Override
@@ -213,11 +211,11 @@ public class AssemblyDeployCallback extends StaticInstallSupport implements Depl
     @Override
     public Bundle installBundle(String region, String uri, InputStream is) throws BundleException {
         // Check blacklist
-        if (bundleBlacklist.isBundleBlacklisted(uri)) {
-            if (builder.getBlacklistPolicy() == Builder.BlacklistPolicy.Fail) {
-                throw new RuntimeException("Bundle " + uri + " is blacklisted");
-            }
-        }
+//        if (bundleBlacklist.isBundleBlacklisted(uri)) {
+//            if (builder.getBlacklistPolicy() == Builder.BlacklistPolicy.Fail) {
+//                throw new RuntimeException("Bundle " + uri + " is blacklisted");
+//            }
+//        }
         // Install
         LOGGER.info("      adding maven artifact: " + uri);
         try {
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 672d166..1c77a84 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
@@ -19,18 +19,23 @@ package org.apache.karaf.profile.assembly;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.*;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Dictionary;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
@@ -49,6 +54,7 @@ import java.util.concurrent.ScheduledExecutorService;
 import java.util.function.Function;
 import java.util.jar.Attributes;
 import java.util.jar.Manifest;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
@@ -76,6 +82,9 @@ import org.apache.karaf.features.internal.repository.BaseRepository;
 import org.apache.karaf.features.internal.resolver.ResourceBuilder;
 import org.apache.karaf.features.internal.service.Blacklist;
 import org.apache.karaf.features.internal.service.Deployer;
+import org.apache.karaf.features.internal.service.FeaturesProcessor;
+import org.apache.karaf.features.internal.service.FeaturesProcessorImpl;
+import org.apache.karaf.features.internal.service.Overrides;
 import org.apache.karaf.features.internal.util.MapUtils;
 import org.apache.karaf.features.internal.util.MultiException;
 import org.apache.karaf.kar.internal.Kar;
@@ -217,6 +226,41 @@ public class Builder {
         }
     }
 
+    /**
+     * Class similar to {@link FeaturePattern} but simplified for profile name matching
+     */
+    private static class ProfileNamePattern {
+        private String name;
+        private Pattern namePattern;
+
+
+        public ProfileNamePattern(String profileName) {
+            if (profileName == null) {
+                throw new IllegalArgumentException("Profile name to match should not be null");
+            }
+            name = profileName;
+            if (name.contains("*")) {
+                namePattern = LocationPattern.toRegExp(name);
+            }
+        }
+
+        /**
+         * Returns <code>if this feature pattern</code> matches given feature/version
+         * @param profileName
+         * @return
+         */
+        public boolean matches(String profileName) {
+            if (profileName == null) {
+                return false;
+            }
+            if (namePattern != null) {
+                return namePattern.matcher(profileName).matches();
+            } else {
+                return name.equals(profileName);
+            }
+        }
+    }
+
     //
     // Input parameters
     //
@@ -229,10 +273,10 @@ public class Builder {
     Map<String, RepositoryInfo> repositories = new LinkedHashMap<>();
     Map<String, Stage> features = new LinkedHashMap<>();
     Map<String, Stage> bundles = new LinkedHashMap<>();
-    List<String> blacklistedProfiles = new ArrayList<>();
-    List<String> blacklistedFeatures = new ArrayList<>();
-    List<String> blacklistedBundles = new ArrayList<>();
-    List<String> blacklistedRepositories = new ArrayList<>();
+    List<String> blacklistedProfileNames = new ArrayList<>();
+    List<String> blacklistedFeatureIdentifiers = new ArrayList<>();
+    List<String> blacklistedBundleURIs = new ArrayList<>();
+    List<String> blacklistedRepositoryURIs = new ArrayList<>();
     BlacklistPolicy blacklistPolicy = BlacklistPolicy.Discard;
     List<String> libraries = new ArrayList<>();
     JavaVersion javase = JavaVersion.Java18;
@@ -259,6 +303,7 @@ public class Builder {
     private KarafPropertyEdits propertyEdits;
     private FeaturesProcessing featuresProcessing = new FeaturesProcessing();
     private Map<String, String> translatedUrls;
+    private Blacklist blacklist;
 
     private Function<MavenResolver, MavenResolver> resolverWrapper = Function.identity();
 
@@ -622,7 +667,7 @@ public class Builder {
      * @return
      */
     public Builder blacklistProfiles(Collection<String> profiles) {
-        this.blacklistedProfiles.addAll(profiles);
+        this.blacklistedProfileNames.addAll(profiles);
         return this;
     }
 
@@ -632,7 +677,7 @@ public class Builder {
      * @return
      */
     public Builder blacklistFeatures(Collection<String> features) {
-        this.blacklistedFeatures.addAll(features);
+        this.blacklistedFeatureIdentifiers.addAll(features);
         return this;
     }
 
@@ -642,7 +687,7 @@ public class Builder {
      * @return
      */
     public Builder blacklistBundles(Collection<String> bundles) {
-        this.blacklistedBundles.addAll(bundles);
+        this.blacklistedBundleURIs.addAll(bundles);
         return this;
     }
 
@@ -652,7 +697,7 @@ public class Builder {
      * @return
      */
     public Builder blacklistRepositories(Collection<String> repositories) {
-        this.blacklistedRepositories.addAll(repositories);
+        this.blacklistedRepositoryURIs.addAll(repositories);
         return this;
     }
 
@@ -724,20 +769,20 @@ public class Builder {
         return this;
     }
 
-    public List<String> getBlacklistedProfiles() {
-        return blacklistedProfiles;
+    public List<String> getBlacklistedProfileNames() {
+        return blacklistedProfileNames;
     }
 
-    public List<String> getBlacklistedFeatures() {
-        return blacklistedFeatures;
+    public List<String> getBlacklistedFeatureIdentifiers() {
+        return blacklistedFeatureIdentifiers;
     }
 
-    public List<String> getBlacklistedBundles() {
-        return blacklistedBundles;
+    public List<String> getBlacklistedBundleURIs() {
+        return blacklistedBundleURIs;
     }
 
-    public List<String> getBlacklistedRepositories() {
-        return blacklistedRepositories;
+    public List<String> getBlacklistedRepositoryURIs() {
+        return blacklistedRepositoryURIs;
     }
 
     public BlacklistPolicy getBlacklistPolicy() {
@@ -807,16 +852,39 @@ public class Builder {
         }
 
         //
+        // Handle blacklist - we'll use SINGLE instance iof Blacklist for all further downloads
+        //
+        blacklist = processBlacklist();
+
+        // we can't yet have full feature processor, because some overrides may be defined in profiles and
+        // profiles are generated after reading features from repositories
+        // so for now, we can only configure blacklisting features processor
+
+        Path existingProcessorDefinition = etcDirectory.resolve("org.apache.karaf.features.xml");
+        String existingProcessorDefinitionURI = null;
+        if (existingProcessorDefinition.toFile().isFile()) {
+            existingProcessorDefinitionURI = existingProcessorDefinition.toFile().toURI().toString();
+        }
+        // now we can configure blacklisting features processor which may have already defined (in XML)
+        // configuration for bundle replacements or feature overrides.
+        // we'll add overrides from profiles later.
+        FeaturesProcessorImpl processor = new FeaturesProcessorImpl(existingProcessorDefinitionURI, blacklist, new HashSet<>());
+
+        //
         // Propagate feature installation from repositories
         //
         LOGGER.info("Loading repositories");
-        Map<String, Features> karRepositories = loadRepositories(manager, repositories.keySet(), false);
+        Map<String, Features> karRepositories = loadRepositories(manager, repositories.keySet(), false, processor);
         for (String repo : repositories.keySet()) {
             RepositoryInfo info = repositories.get(repo);
             if (info.addAll) {
-                LOGGER.info("   adding all features from repository: " + repo + " (stage: " + info.stage + ")");
+                LOGGER.info("   adding all non-blacklisted features from repository: " + repo + " (stage: " + info.stage + ")");
                 for (Feature feature : karRepositories.get(repo).getFeature()) {
-                    features.put(feature.getId(), info.stage);
+                    if (feature.isBlacklisted()) {
+                        LOGGER.info("      feature {}/{} is blacklisted - skipping.", feature.getId(), feature.getVersion());
+                    } else {
+                        features.put(feature.getId(), info.stage);
+                    }
                 }
             }
         }
@@ -868,6 +936,14 @@ public class Builder {
         // so property placeholders are preserved - like ${karaf.base})
         Profile overallEffective = Profiles.getEffective(overallOverlay, false);
 
+        //
+        // Handle overrides - existing (unzipped from KAR) and defined in profile
+        //
+        Set<String> overrides = processOverrides(overallEffective.getOverrides());
+
+        // we can now add overrides from profiles.
+        processor.addOverrides(overrides);
+
         if (writeProfiles) {
             Path profiles = etcDirectory.resolve("profiles");
             LOGGER.info("Adding profiles to {}", homeDirectory.relativize(profiles));
@@ -938,57 +1014,101 @@ public class Builder {
             editor.run();
         }
 
-        //
-        // Handle overrides
-        //
-        if (!overallEffective.getOverrides().isEmpty()) {
-            Path overrides = etcDirectory.resolve("overrides.properties");
-            List<String> lines = new ArrayList<>();
-            lines.add("#");
-            lines.add("# Generated by the karaf assembly builder");
-            lines.add("#");
-            lines.addAll(overallEffective.getOverrides());
-            LOGGER.info("Generating {}", homeDirectory.relativize(overrides));
-            Files.write(overrides, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
-        }
-
-        //
-        // Handle blacklist
-        //
-        if (!blacklistedFeatures.isEmpty() || !blacklistedBundles.isEmpty()) {
-            Path blacklist = etcDirectory.resolve("blacklisted.properties");
-            List<String> lines = new ArrayList<>();
-            lines.add("#");
-            lines.add("# Generated by the karaf assembly builder");
-            lines.add("#");
-            if (!blacklistedFeatures.isEmpty()) {
-                lines.add("");
-                lines.add("# Features");
-                lines.addAll(blacklistedFeatures);
-            }
-            if (!blacklistedBundles.isEmpty()) {
-                lines.add("");
-                lines.add("# Bundles");
-                lines.addAll(blacklistedBundles);
+//        if (!blacklistedFeatures.isEmpty() || !blacklistedBundles.isEmpty()) {
+//            List<String> lines = new ArrayList<>();
+//            lines.add("#");
+//            lines.add("# Generated by the karaf assembly builder");
+//            lines.add("#");
+//            if (!blacklistedFeatures.isEmpty()) {
+//                lines.add("");
+//                lines.add("# Features");
+//                lines.addAll(blacklistedFeatures);
+//            }
+//            if (!blacklistedBundles.isEmpty()) {
+//                lines.add("");
+//                lines.add("# Bundles");
+//                lines.addAll(blacklistedBundles);
+//            }
+//            LOGGER.info("Generating {}", homeDirectory.relativize(blacklist));
+//            Files.write(blacklist, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+//        }
+
+        // TODO: download overrides, implement fuller override clauses (original->replacement)
+        if (processor.hasInstructions()) {
+            Path featuresProcessingXml = etcDirectory.resolve("org.apache.karaf.features.xml");
+            try (FileOutputStream fos = new FileOutputStream(featuresProcessingXml.toFile())) {
+                processor.writeInstructions(fos);
             }
-            LOGGER.info("Generating {}", homeDirectory.relativize(blacklist));
-            Files.write(blacklist, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
         }
 
         //
         // Startup stage
         //
-        Profile startupEffective = startupStage(startupProfile);
+        Profile startupEffective = startupStage(startupProfile, processor);
 
         //
         // Boot stage
         //
-        Set<Feature> allBootFeatures = bootStage(bootProfile, startupEffective);
+        Set<Feature> allBootFeatures = bootStage(bootProfile, startupEffective, processor);
 
         //
         // Installed stage
         //
-        installStage(installedProfile, allBootFeatures);
+        installStage(installedProfile, allBootFeatures, processor);
+    }
+
+    /**
+     * Checks existing (etc/overrides.properties) and configured (in profiles) overrides definitions
+     * @param profileOverrides
+     * @return
+     */
+    private Set<String> processOverrides(List<String> profileOverrides) {
+        Set<String> result = new LinkedHashSet<>();
+        Path existingOverridesLocation = etcDirectory.resolve("overrides.properties");
+        if (existingOverridesLocation.toFile().isFile()) {
+            LOGGER.warn("Found {} which is deprecated, please use new feature processor configuration.", homeDirectory.relativize(existingOverridesLocation));
+            result.addAll(Overrides.loadOverrides(existingOverridesLocation.toFile().toURI().toString()));
+        }
+        result.addAll(profileOverrides);
+
+        return result;
+    }
+
+    /**
+     * Checks existing and configured blacklisting definitions
+     * @return
+     * @throws IOException
+     */
+    private Blacklist processBlacklist() throws IOException {
+        Blacklist existingBlacklist = null;
+        Blacklist blacklist = new Blacklist();
+        Path existingBLacklistedLocation = etcDirectory.resolve("blacklisted.properties");
+        if (existingBLacklistedLocation.toFile().isFile()) {
+            LOGGER.warn("Found {} which is deprecated, please use new feature processor configuration.", homeDirectory.relativize(existingBLacklistedLocation));
+            existingBlacklist = new Blacklist(Files.readAllLines(existingBLacklistedLocation));
+        }
+        for (String br : blacklistedRepositoryURIs) {
+            try {
+                blacklist.blacklistRepository(new LocationPattern(br));
+            } catch (MalformedURLException e) {
+                LOGGER.warn("Blacklisted features XML repository URI is invalid {}, ignoring", br);
+            }
+        }
+        for (String bf : blacklistedFeatureIdentifiers) {
+            blacklist.blacklistFeature(new FeaturePattern(bf));
+        }
+        for (String bb : blacklistedBundleURIs) {
+            try {
+                blacklist.blacklistBundle(new LocationPattern(bb));
+            } catch (MalformedURLException e) {
+                LOGGER.warn("Blacklisted bundle URI is invalid {}, ignoring", bb);
+            }
+        }
+        if (existingBlacklist != null) {
+            blacklist.merge(existingBlacklist);
+        }
+
+        return blacklist;
     }
 
     private MavenResolver createMavenResolver() {
@@ -1012,6 +1132,8 @@ public class Builder {
      */
     private Map<String, Profile> loadExternalProfiles(List<String> profilesUris) throws IOException, MultiException, InterruptedException {
         Map<String, Profile> profiles = new LinkedHashMap<>();
+        Map<String, Profile> filteredProfiles = new LinkedHashMap<>();
+
         for (String profilesUri : profilesUris) {
             String uri = profilesUri;
             if (uri.startsWith("jar:") && uri.contains("!/")) {
@@ -1035,19 +1157,33 @@ public class Builder {
             }
             profiles.putAll(Profiles.loadProfiles(profilePath));
             // Handle blacklisted profiles
-            if (!blacklistedProfiles.isEmpty()) {
-                if (blacklistPolicy == BlacklistPolicy.Discard) {
-                    // Override blacklisted profiles with empty ones
-                    for (String profile : blacklistedProfiles) {
-                        profiles.put(profile, ProfileBuilder.Factory.create(profile).getProfile());
+            List<ProfileNamePattern> blacklistedProfilePatterns = blacklistedProfileNames.stream()
+                    .map(ProfileNamePattern::new).collect(Collectors.toList());
+
+            for (String profileName : profiles.keySet()) {
+                boolean blacklisted = false;
+                for (ProfileNamePattern pattern : blacklistedProfilePatterns) {
+                    if (pattern.matches(profileName)) {
+                        LOGGER.info("   blacklisting profile {} from {}", profileName, profilePath);
+                        // TODO review blacklist policy options
+                        if (blacklistPolicy == BlacklistPolicy.Discard) {
+                            // Override blacklisted profiles with empty one
+                            filteredProfiles.put(profileName, ProfileBuilder.Factory.create(profileName).getProfile());
+                        } else {
+                            // Remove profile completely
+                        }
+                        // no need to check other patterns
+                        blacklisted = true;
+                        break;
                     }
-                } else {
-                    // Remove profiles completely
-                    profiles.keySet().removeAll(blacklistedProfiles);
+                }
+                if (!blacklisted) {
+                    filteredProfiles.put(profileName, profiles.get(profileName));
                 }
             }
         }
-        return profiles;
+
+        return filteredProfiles;
     }
 
     private void reformatClauses(Properties config, String key) {
@@ -1137,18 +1273,18 @@ public class Builder {
                         if (packages != null) {
                             Clause[] clauses1 = org.apache.felix.utils.manifest.Parser.parseHeader(packages);
                             if (export) {
-                                String val = config.getProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA);
+                                StringBuilder val = new StringBuilder(config.getProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA));
                                 for (Clause clause1 : clauses1) {
-                                    val += "," + clause1.toString();
+                                    val.append(",").append(clause1.toString());
                                 }
-                                config.setProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, val);
+                                config.setProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, val.toString());
                             }
                             if (delegate) {
-                                String val = config.getProperty(Constants.FRAMEWORK_BOOTDELEGATION);
+                                StringBuilder val = new StringBuilder(config.getProperty(Constants.FRAMEWORK_BOOTDELEGATION));
                                 for (Clause clause1 : clauses1) {
-                                    val += "," + clause1.getName();
+                                    val.append(",").append(clause1.toString());
                                 }
-                                config.setProperty(Constants.FRAMEWORK_BOOTDELEGATION, val);
+                                config.setProperty(Constants.FRAMEWORK_BOOTDELEGATION, val.toString());
                             }
                         }
                     }
@@ -1156,7 +1292,7 @@ public class Builder {
         }
     }
 
-    private void installStage(Profile installedProfile, Set<Feature> allBootFeatures) throws Exception {
+    private void installStage(Profile installedProfile, Set<Feature> allBootFeatures, FeaturesProcessor processor) throws Exception {
         LOGGER.info("Install stage");
         //
         // Handle installed profiles
@@ -1168,7 +1304,7 @@ public class Builder {
 
         // Load startup repositories
         LOGGER.info("   Loading repositories");
-        Map<String, Features> installedRepositories = loadRepositories(manager, installedEffective.getRepositories(), true);
+        Map<String, Features> installedRepositories = loadRepositories(manager, installedEffective.getRepositories(), true, processor);
         // Compute startup feature dependencies
         Set<Feature> allInstalledFeatures = new HashSet<>();
         for (Features repo : installedRepositories.values()) {
@@ -1179,7 +1315,7 @@ public class Builder {
         allInstalledFeatures.addAll(allBootFeatures);
         FeatureSelector selector = new FeatureSelector(allInstalledFeatures);
         Set<Feature> installedFeatures = selector.getMatching(installedEffective.getFeatures());
-        ArtifactInstaller installer = new ArtifactInstaller(systemDirectory, downloader, blacklistedBundles);
+        ArtifactInstaller installer = new ArtifactInstaller(systemDirectory, downloader, blacklist);
         for (Feature feature : installedFeatures) {
             LOGGER.info("   Feature {} is defined as an installed feature", feature.getId());
             for (Bundle bundle : feature.getBundle()) {
@@ -1205,7 +1341,7 @@ public class Builder {
         downloader.await();
     }
 
-    private Set<Feature> bootStage(Profile bootProfile, Profile startupEffective) throws Exception {
+    private Set<Feature> bootStage(Profile bootProfile, Profile startupEffective, FeaturesProcessor processor) throws Exception {
         LOGGER.info("Boot stage");
         //
         // Handle boot profiles
@@ -1214,7 +1350,7 @@ public class Builder {
         Profile bootEffective = Profiles.getEffective(bootOverlay, false);
         // Load startup repositories
         LOGGER.info("   Loading repositories");
-        Map<String, Features> bootRepositories = loadRepositories(manager, bootEffective.getRepositories(), true);
+        Map<String, Features> bootRepositories = loadRepositories(manager, bootEffective.getRepositories(), true, processor);
         // Compute startup feature dependencies
         Set<Feature> allBootFeatures = new HashSet<>();
         for (Features repo : bootRepositories.values()) {
@@ -1276,7 +1412,7 @@ public class Builder {
             prereqs.put("spring:", Arrays.asList("deployer", "spring"));
             prereqs.put("wrap:", Collections.singletonList("wrap"));
             prereqs.put("war:", Collections.singletonList("war"));
-            ArtifactInstaller installer = new ArtifactInstaller(systemDirectory, downloader, blacklistedBundles);
+            ArtifactInstaller installer = new ArtifactInstaller(systemDirectory, downloader, blacklist);
             for (String location : locations) {
                 installer.installArtifact(location);
                 for (Map.Entry<String, List<String>> entry : prereqs.entrySet()) {
@@ -1365,8 +1501,6 @@ public class Builder {
         return allBootFeatures;
     }
 
-
-
     private String getRepos(Features rep) {
         StringBuilder repos = new StringBuilder();
         for (String repo : new HashSet<>(rep.getRepository())) {
@@ -1424,7 +1558,7 @@ public class Builder {
         return dep;
     }
 
-    private Profile startupStage(Profile startupProfile) throws Exception {
+    private Profile startupStage(Profile startupProfile, FeaturesProcessor processor) throws Exception {
         LOGGER.info("Startup stage");
         //
         // Compute startup
@@ -1433,7 +1567,7 @@ public class Builder {
         Profile startupEffective = Profiles.getEffective(startupOverlay, false);
         // Load startup repositories
         LOGGER.info("   Loading repositories");
-        Map<String, Features> startupRepositories = loadRepositories(manager, startupEffective.getRepositories(), false);
+        Map<String, Features> startupRepositories = loadRepositories(manager, startupEffective.getRepositories(), false, processor);
 
         //
         // Resolve
@@ -1511,21 +1645,15 @@ public class Builder {
         return staged;
     }
 
-    private Map<String, Features> loadRepositories(DownloadManager manager, Collection<String> repositories, final boolean install) throws Exception {
+    private Map<String, Features> loadRepositories(DownloadManager manager, Collection<String> repositories, final boolean install, FeaturesProcessor processor) throws Exception {
         final Map<String, Features> loaded = new HashMap<>();
         final Downloader downloader = manager.createDownloader();
-        final List<String> blacklist = new ArrayList<>();
-        blacklist.addAll(blacklistedBundles);
-        blacklist.addAll(blacklistedFeatures);
-        final List<String> blacklistRepos = new ArrayList<>(blacklistedRepositories);
-        final Blacklist blacklistOther = new Blacklist(blacklist);
-        final Blacklist repoBlacklist = new Blacklist(blacklistRepos);
         for (String repository : repositories) {
             downloader.download(repository, new DownloadCallback() {
                 @Override
                 public void downloaded(final StreamProvider provider) throws Exception {
                     String url = provider.getUrl();
-                    if (repoBlacklist.isRepositoryBlacklisted(url)) {
+                    if (processor.isRepositoryBlacklisted(url)) {
                         LOGGER.info("   feature repository " + url + " is blacklisted");
                         return;
                     }
@@ -1541,9 +1669,11 @@ public class Builder {
                             }
                             try (InputStream is = provider.open()) {
                                 Features featuresModel = JaxbUtil.unmarshal(url, is, false);
-                                if (blacklistPolicy == BlacklistPolicy.Discard) {
-                                    blacklistOther.blacklist(featuresModel);
-                                }
+                                // always process according to processor configuration
+                                processor.process(featuresModel);
+                                // TODO consult blacklist policy
+//                                if (blacklistPolicy == BlacklistPolicy.Discard) {
+//                                }
                                 loaded.put(provider.getUrl(), featuresModel);
                                 for (String innerRepository : featuresModel.getRepository()) {
                                     downloader.download(innerRepository, this);
@@ -1598,7 +1728,7 @@ public class Builder {
         Deployer deployer = new Deployer(manager, resolver, callback);
 
         // Install framework
-        Deployer.DeploymentRequest request = createDeploymentRequest();
+        Deployer.DeploymentRequest request = Deployer.DeploymentRequest.defaultDeploymentRequest();
         // Add overrides
         request.overrides.addAll(overrides);
         // Add optional resources
@@ -1638,18 +1768,6 @@ public class Builder {
         return callback.getStartupBundles();
     }
 
-    private Deployer.DeploymentRequest createDeploymentRequest() {
-        Deployer.DeploymentRequest request = new Deployer.DeploymentRequest();
-        request.bundleUpdateRange = FeaturesService.DEFAULT_BUNDLE_UPDATE_RANGE;
-        request.featureResolutionRange = FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE;
-        request.serviceRequirements = FeaturesService.SERVICE_REQUIREMENTS_DEFAULT;
-        request.overrides = new HashSet<>();
-        request.requirements = new HashMap<>();
-        request.stateChanges = new HashMap<>();
-        request.options = EnumSet.noneOf(FeaturesService.Option.class);
-        return request;
-    }
-
     @SuppressWarnings("rawtypes")
     private BundleRevision getSystemBundle() throws Exception {
         Path configPropPath = etcDirectory.resolve("config.properties");
diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java
index f8d78b9..08f41cb 100644
--- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java
+++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java
@@ -149,6 +149,12 @@ public class AssemblyMojo extends MojoSupport {
     private String environment;
 
     /**
+     * Default start level for bundles in features that don't specify it.
+     */
+    @Parameter
+    protected int defaultStartLevel = 30;
+
+    /**
      * List of compile-scope features XML files to be used in startup stage (etc/startup.properties)
      */
     @Parameter
@@ -455,6 +461,7 @@ public class AssemblyMojo extends MojoSupport {
         builder.pidsToExtract(pidsToExtract);
         builder.writeProfiles(writeProfiles);
         builder.environment(environment);
+        builder.defaultStartLevel(defaultStartLevel);
 
         // Set up remote repositories from Maven build, to be used by pax-url-aether resolver
         String remoteRepositories = MavenUtil.remoteRepositoryList(project.getRemoteProjectRepositories());
diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java
index d13e786..3aca9d3 100644
--- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java
+++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java
@@ -420,7 +420,7 @@ public class VerifyMojo extends MojoSupport {
 
 
             // Install framework
-            Deployer.DeploymentRequest request = createDeploymentRequest();
+            Deployer.DeploymentRequest request = Deployer.DeploymentRequest.defaultDeploymentRequest();
 
             for (String fmwk : framework) {
                 MapUtils.addToMapSet(request.requirements, FeaturesService.ROOT_REGION, fmwk);
@@ -491,18 +491,6 @@ public class VerifyMojo extends MojoSupport {
         }
     }
 
-    private static Deployer.DeploymentRequest createDeploymentRequest() {
-        Deployer.DeploymentRequest request = new Deployer.DeploymentRequest();
-        request.bundleUpdateRange = FeaturesService.DEFAULT_BUNDLE_UPDATE_RANGE;
-        request.featureResolutionRange = FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE;
-        request.serviceRequirements = FeaturesService.SERVICE_REQUIREMENTS_DEFAULT;
-        request.overrides = new HashSet<>();
-        request.requirements = new HashMap<>();
-        request.stateChanges = new HashMap<>();
-        request.options = EnumSet.noneOf(FeaturesService.Option.class);
-        return request;
-    }
-
     private static String toString(Collection<String> collection) {
         StringBuilder sb = new StringBuilder();
         sb.append("{\n");
diff --git a/util/src/main/java/org/apache/karaf/util/xml/IndentingXMLEventWriter.java b/util/src/main/java/org/apache/karaf/util/xml/IndentingXMLEventWriter.java
new file mode 100755
index 0000000..35735f2
--- /dev/null
+++ b/util/src/main/java/org/apache/karaf/util/xml/IndentingXMLEventWriter.java
@@ -0,0 +1,144 @@
+/*
+ * 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.util.xml;
+
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.stream.XMLEventFactory;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLEventWriter;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.XMLEvent;
+
+public class IndentingXMLEventWriter implements XMLEventWriter {
+
+    private static final XMLEventFactory factory = XMLEventFactory.newFactory();
+
+    private XMLEventWriter wrappedWriter;
+    private int depth = 0;
+
+    private boolean newLineBeforeStartElement = false;
+    private boolean indentBeforeEndElement = false;
+
+    private String indentationString = "    ";
+
+    /**
+     * @param wrappedWriter
+     * @param indentation
+     */
+    public IndentingXMLEventWriter(XMLEventWriter wrappedWriter, String indentation) {
+        this.wrappedWriter = wrappedWriter;
+        this.indentationString = indentation;
+    }
+
+    @Override
+    public void close() throws XMLStreamException {
+        this.wrappedWriter.close();
+    }
+
+    @Override
+    public void add(XMLEvent event) throws XMLStreamException {
+        switch (event.getEventType()) {
+            case XMLStreamConstants.START_DOCUMENT:
+                this.wrappedWriter.add(event);
+                this.wrappedWriter.add(factory.createCharacters("\n"));
+                break;
+            case XMLStreamConstants.START_ELEMENT:
+                if (this.newLineBeforeStartElement)
+                    this.wrappedWriter.add(factory.createCharacters("\n"));
+                this.newLineBeforeStartElement = true;
+                this.indentBeforeEndElement = false;
+                this.possiblyIndent();
+                this.wrappedWriter.add(event);
+                this.depth++;
+                break;
+            case XMLStreamConstants.END_ELEMENT:
+                this.newLineBeforeStartElement = false;
+                this.depth--;
+                if (this.indentBeforeEndElement)
+                    this.possiblyIndent();
+                this.indentBeforeEndElement = true;
+                this.wrappedWriter.add(event);
+                this.wrappedWriter.add(factory.createCharacters("\n"));
+                break;
+            case XMLStreamConstants.COMMENT:
+            case XMLStreamConstants.PROCESSING_INSTRUCTION:
+                this.wrappedWriter.add(event);
+                this.wrappedWriter.add(factory.createCharacters("\n"));
+                this.newLineBeforeStartElement = false;
+                this.indentBeforeEndElement = true;
+                break;
+            default:
+                this.wrappedWriter.add(event);
+        }
+    }
+
+    /**
+     * Indent at non-zero depth
+     * @throws XMLStreamException
+     */
+    private void possiblyIndent() throws XMLStreamException {
+        if (this.depth > 0) {
+            StringBuffer sb = new StringBuffer();
+            for (int i = 0; i < this.depth; i++)
+                sb.append(this.indentationString);
+            this.wrappedWriter.add(factory.createCharacters(sb.toString()));
+        }
+    }
+
+    @Override
+    public void add(XMLEventReader reader) throws XMLStreamException {
+        this.wrappedWriter.add(reader);
+    }
+
+    @Override
+    public String getPrefix(String uri) throws XMLStreamException {
+        return this.wrappedWriter.getPrefix(uri);
+    }
+
+    @Override
+    public void setPrefix(String prefix, String uri) throws XMLStreamException {
+        this.wrappedWriter.setPrefix(prefix, uri);
+    }
+
+    @Override
+    public void setDefaultNamespace(String uri) throws XMLStreamException {
+        this.wrappedWriter.setDefaultNamespace(uri);
+    }
+
+    @Override
+    public void setNamespaceContext(NamespaceContext context) throws XMLStreamException {
+        this.wrappedWriter.setNamespaceContext(context);
+    }
+
+    @Override
+    public NamespaceContext getNamespaceContext() {
+        return this.wrappedWriter.getNamespaceContext();
+    }
+
+    @Override
+    public void flush() throws XMLStreamException {
+        this.wrappedWriter.flush();
+    }
+
+    public void setIndentationString(String indentationString) {
+        this.indentationString = indentationString;
+    }
+
+}

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

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

Posted by gg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

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

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

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

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

[karaf] 06/11: [KARAF-5468] Cleaning up AssemblyMojo, Profiles and profile Builder

Posted by gg...@apache.org.
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 dd1990c5059136d923f7735be84c577679914b90
Author: Grzegorz Grzybek <gr...@gmail.com>
AuthorDate: Wed Nov 15 15:43:01 2017 +0100

    [KARAF-5468] Cleaning up AssemblyMojo, Profiles and profile Builder
    
    * Added good amount of documentation for public API, Maven mojo
    parameters and interfaces
    * Removed unused karaf-maven-plugin:assembly parameters
    * Renamed some of karaf-maven-plugin:assembly parameters
    * Changed some of karaf-maven-plugin:assembly parameters from String to
    List<String>
    * Changed some string constants into enums (Builder.JavaVersion)
    * Refactored AssemblyMojo
    * Added new logging statements to AssemblyMojo and Builder
    * Renamed "agent" to "profile" wherever possible
---
 demos/profiles/dynamic/pom.xml                     |   6 +-
 demos/profiles/static/pom.xml                      |   6 +-
 profile/pom.xml                                    |   1 +
 .../java/org/apache/karaf/profile/Profile.java     | 129 ++--
 .../org/apache/karaf/profile/ProfileConstants.java | 128 ++++
 .../org/apache/karaf/profile/assembly/Builder.java | 604 +++++++++++++++---
 .../karaf/profile/command/ProfileDisplay.java      |  14 +-
 .../apache/karaf/profile/command/ProfileEdit.java  |  28 +-
 .../karaf/profile/impl/ProfileBuilderImpl.java     |  28 +-
 .../org/apache/karaf/profile/impl/ProfileImpl.java |  72 ++-
 .../org/apache/karaf/profile/impl/Profiles.java    |  99 ++-
 .../apache/karaf/profile/impl/ProfilesTest.java    | 142 +++++
 tooling/karaf-maven-plugin/pom.xml                 |   4 -
 .../org/apache/karaf/tooling/AssemblyMojo.java     | 678 +++++++++++++--------
 .../java/org/apache/karaf/tooling/VerifyMojo.java  |  28 +-
 .../org/apache/karaf/tooling/utils/MavenUtil.java  |  32 +-
 16 files changed, 1496 insertions(+), 503 deletions(-)

diff --git a/demos/profiles/dynamic/pom.xml b/demos/profiles/dynamic/pom.xml
index 9c2ec36..ce8f734 100644
--- a/demos/profiles/dynamic/pom.xml
+++ b/demos/profiles/dynamic/pom.xml
@@ -106,9 +106,9 @@
                     </execution>
                 </executions>
                 <configuration>
-                    <profilesUri>
-                        jar:mvn:org.apache.karaf.demos.profiles/registry/${project.version}!/
-                    </profilesUri>
+                    <profilesUris>
+                        <uri>jar:mvn:org.apache.karaf.demos.profiles/registry/${project.version}!/</uri>
+                    </profilesUris>
                     <bootFeatures>
                         <feature>deployer</feature>
                     </bootFeatures>
diff --git a/demos/profiles/static/pom.xml b/demos/profiles/static/pom.xml
index 0542c6b..1250085 100644
--- a/demos/profiles/static/pom.xml
+++ b/demos/profiles/static/pom.xml
@@ -122,9 +122,9 @@
                 <configuration>
                     <useReferenceUrls>true</useReferenceUrls>
                     <environment>static</environment>
-                    <profilesUri>
-                        jar:mvn:org.apache.karaf.demos.profiles/registry/${project.version}!/
-                    </profilesUri>
+                    <profilesUris>
+                        <uri>jar:mvn:org.apache.karaf.demos.profiles/registry/${project.version}!/</uri>
+                    </profilesUris>
                     <startupProfiles>
                         <profile>karaf</profile>
                         <profile>example-loanbroker-bank1</profile>
diff --git a/profile/pom.xml b/profile/pom.xml
index 223e522..118a14f 100644
--- a/profile/pom.xml
+++ b/profile/pom.xml
@@ -183,6 +183,7 @@
                             org.apache.karaf.profile.impl,
                             org.apache.karaf.profile.impl.osgi,
                             org.apache.karaf.profile.versioning,
+                            org.apache.karaf.util,
                             org.apache.karaf.util.config,
                             org.apache.karaf.util.maven,
                             org.apache.felix.utils.manifest,
diff --git a/profile/src/main/java/org/apache/karaf/profile/Profile.java b/profile/src/main/java/org/apache/karaf/profile/Profile.java
index c9d4b3d..ca08bd2 100644
--- a/profile/src/main/java/org/apache/karaf/profile/Profile.java
+++ b/profile/src/main/java/org/apache/karaf/profile/Profile.java
@@ -21,88 +21,135 @@ import java.util.Map;
 import java.util.Set;
 
 /**
- * The immutable view of a profile
+ * <p>A <em>profile</em> is a container for configuration that can be applied to Karaf distribution.</p>
+ *
+ * <p>Profiles may inherit from other (single or multiple) profiles. An <em>overlay</em> profile is single
+ * profile with all the configurations, attributes and files from parent profiles, while configurations,
+ * attributes and files from <em>child</em> profile overwrites corresponding data from parent profiles.</p>
+ *
+ * <p>Configuration include:<ul>
+ *     <li>Attributes</li>
+ *     <li>ConfigAdmin configurations (PIDs) to put into <code>${karaf.etc}</code> directory</li>
+ *     <li>Other resources to put into <code>${karaf.etc}</code> directory</li>
+ * </ul></p>
+ *
+ * <p>Attributes are properties in special file <code>profile.cfg</code> (<code>profile</code> PID) and may specify:<ul>
+ *     <li>OSGi bundles to install (prefix: <code>bundle.</code>)</li>
+ *     <li>Karaf features to install (prefix: <code>feature.</code>)</li>
+ *     <li>Feature XML repositories to use to resolve bundles and features (prefix: <code>repository.</code>)</li>
+ *     <li>Identifiers of parent profiles (property name: <code>attribute.parents</code>)</li>
+ *     <li>Indication of abstract profile (property name: <code>abstract</code>)</li>
+ *     <li>Indication of hidden profile (property name: <code>hidden</code>)</li>
+ *     <li>Different attributes (prefix: <code>attribute.</code>)</li>
+ *     <li>Properties to be added to <code>etc/config.properties</code> (prefix: <code>config.</code>)</li>
+ *     <li>Properties to be added to <code>etc/system.properties</code> (prefix: <code>system.</code>)</li>
+ *     <li>Additional libraries to be added to <code>lib</code> (prefix: <code>library.</code>)</li>
+ *     <li>Additional libraries to be added to <code>lib/boot</code> (prefix: <code>boot.</code>)</li>
+ *     <li>Additional libraries to be added to <code>lib/endorsed</code> (prefix: <code>endorsed.</code>)</li>
+ *     <li>Additional libraries to be added to <code>lib/ext</code> (prefix: <code>ext.</code>)</li>
+ *     <li>Bundle override definitions to be added to <code>etc/overrides.properties</code> (prefix: <code>override.</code>)</li>
+ *     <li>Optional {@link org.osgi.resource.Resource resources} to be used during resolution (prefix: <code>optional.</code>)</li>
+ * </ul></p>
  */
-public interface Profile {
+public interface Profile extends ProfileConstants {
 
     /**
-     * The attribute key for the list of parents
+     * Returns an attribute map of this profile
+     * @return
      */
-    String PARENTS = "parents";
+    Map<String, String> getAttributes();
 
     /**
-     * The attribute key for the description of the profile
+     * Returns a property map for additional properties to be added to <code>${karaf.etc}/config.properties</code>
+     * @return
      */
-    String DESCRIPTION = "description";
+    Map<String, String> getConfig();
 
     /**
-     * The attribute key for the abstract flag
+     * Returns a property map for additional properties to be added to <code>${karaf.etc}/system.properties</code>
+     * @return
      */
-    String ABSTRACT = "abstract";
+    Map<String, String> getSystem();
 
     /**
-     * The attribute key for the hidden flag
+     * Returns a unique identifier of this profile
+     * @return
      */
-    String HIDDEN = "hidden";
+    String getId();
 
     /**
-     * Key indicating a deletion.
-     * This value can appear as the value of a key in a configuration
-     * or as a key itself.  If used as a key, the whole configuration
-     * is flagged has been deleted from its parent when computing the
-     * overlay.
+     * Returns a list of parent profile identifiers for this profile
+     * @return
      */
-    String DELETED = "#deleted#";
+    List<String> getParentIds();
 
     /**
-     * The pid of the configuration holding internal profile attributes
+     * Returns a list of bundles (bundle URIs) defined in this profile
+     * @return
      */
-    String INTERNAL_PID = "profile";
+    List<String> getBundles();
 
     /**
-     * The file suffix for a configuration
+     * Returns a list of features (<code>feature-name[/feature-version]</code>) defined in this profile
+     * @return
      */
-    String PROPERTIES_SUFFIX = ".cfg";
+    List<String> getFeatures();
 
     /**
-     * The attribute prefix for in the agent configuration
+     * Returns a list of features XML repositories (URIs) defined in this profile
+     * @return
      */
-    String ATTRIBUTE_PREFIX = "attribute.";
+    List<String> getRepositories();
 
     /**
-     * The config prefix for in the agent configuration
+     * Returns a list of libraries (to be added to <code>${karaf.home}/lib</code>) defined in this profile
+     * @return
      */
-    String CONFIG_PREFIX = "config.";
+    List<String> getLibraries();
 
     /**
-     * The config prefix for in the agent configuration
+     * Returns a list of boot libraries (to be added to <code>${karaf.home}/lib/boot</code>) defined in this profile
+     * @return
      */
-    String SYSTEM_PREFIX = "system.";
+    List<String> getBootLibraries();
 
-    Map<String, String> getAttributes();
-    Map<String, String> getConfig();
-    Map<String, String> getSystem();
+    /**
+     * Returns a list of endorsed libraries (to be added to <code>${karaf.home}/lib/endorsed</code>) defined in this profile
+     * @return
+     */
+    List<String> getEndorsedLibraries();
 
-    List<String> getParentIds();
+    /**
+     * Returns a list of extension libraries (to be added to <code>${karaf.home}/lib/ext</code>) defined in this profile
+     * @return
+     */
+    List<String> getExtLibraries();
 
-    List<String> getLibraries();
-    List<String> getBundles();
-    List<String> getFeatures();
-    List<String> getRepositories();
+    /**
+     * Returns a list of bundle override definitions (to be added to <code>${karaf.etc}/overrides.properties</code>)
+     * defined in this profile
+     * @return
+     */
     List<String> getOverrides();
-    List<String> getOptionals();
 
-    String getId();
+    /**
+     * Returns a list of optional {@link org.osgi.resource.Resource resources} (URIs) to be used during
+     * resolution
+     * @return
+     */
+    List<String> getOptionals();
 
     /**
-     * Get the configuration file names that are available on this profile.
+     * Get the configuration file names that are available on this profile. This list should contain at least
+     * <code>profile.cfg</code> file.
      *
      * @return The configuration file names in the profile.
      */
     Set<String> getConfigurationFileNames();
 
     /**
-     * Get all file configurations.
+     * Get all file configurations. This list should contain at least
+     * <code>profile.cfg</code> file.
      *
      * @return The file configurations in the profile.
      */
@@ -117,7 +164,8 @@ public interface Profile {
     byte[] getFileConfiguration(String fileName);
 
     /**
-     * Get all configuration properties.
+     * Get all configuration properties.This list should contain at least
+     * configuration from main profile file - <code>profile.cfg</code>.
      *
      * @return The configurations in the profile.
      */
@@ -132,7 +180,8 @@ public interface Profile {
     Map<String, Object> getConfiguration(String pid);
 
     /**
-     * Indicate if this profile is an overlay or not.
+     * Indicate if this profile is an overlay or not. An <em>overlay</em> profile includes configurations and
+     * attributes of parent profiles, while descendant profiles always have priority over parent profiles.
      *
      * @return True if the profile is an overlay, false else.
      */
diff --git a/profile/src/main/java/org/apache/karaf/profile/ProfileConstants.java b/profile/src/main/java/org/apache/karaf/profile/ProfileConstants.java
new file mode 100644
index 0000000..fe9417d
--- /dev/null
+++ b/profile/src/main/java/org/apache/karaf/profile/ProfileConstants.java
@@ -0,0 +1,128 @@
+/*
+ * 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.profile;
+
+public interface ProfileConstants {
+
+    /**
+     * The attribute prefix for the profile configuration (<code>profile.cfg</code>)
+     */
+    String ATTRIBUTE_PREFIX = "attribute.";
+
+    /**
+     * The attribute key for whitespace-separated list of parent profile IDs
+     */
+    String PARENTS = ATTRIBUTE_PREFIX + "parents";
+
+    /**
+     * The attribute key for the description of the profile
+     */
+    String DESCRIPTION = "description";
+
+    /**
+     * The attribute key for the <em>abstract</em> flag
+     */
+    String ABSTRACT = "abstract";
+
+    /**
+     * The attribute key for the <em>hidden</em> flag
+     */
+    String HIDDEN = "hidden";
+
+    /**
+     * <p>Key indicating a deletion.</p>
+     * <p>This value can appear as the value of a key in a configuration
+     * or as a key itself.  If used as a key, the whole configuration
+     * is flagged as deleted from its parent when computing the overlay.</p>
+     */
+    String DELETED = "#deleted#";
+
+    /**
+     * The pid of the configuration holding internal profile attributes
+     */
+    String INTERNAL_PID = "profile";
+
+    /**
+     * The file suffix for a configuration
+     */
+    String PROPERTIES_SUFFIX = ".cfg";
+
+    /**
+     * The prefix for attributes that are targeted for <code>${karaf.etc}/config.properties</code> file
+     */
+    String CONFIG_PREFIX = "config.";
+
+    /**
+     * The prefix for attributes that are targeted for <code>${karaf.etc}/system.properties</code> file
+     */
+    String SYSTEM_PREFIX = "system.";
+
+    /**
+     * The prefix for attributes that specify URIs of features XML files
+     */
+    String REPOSITORY_PREFIX = "repository.";
+
+    /**
+     * The prefix for attributes that specify feature names (<code>name[/version]</code>) to install/use
+     */
+    String FEATURE_PREFIX = "feature.";
+
+    /**
+     * The prefix for attributes that specify bundle URIs to install
+     */
+    String BUNDLE_PREFIX = "bundle.";
+
+    /**
+     * The prefix for attributes that specify additional libraries to add to <code>${karaf.home}/lib</code>.
+     * These are native libraries only. JARs that should be available in app classpath should go to
+     * <code>${karaf.home}/lib/boot</code> and use {@link #BOOT_PREFIX}.
+     */
+    String LIB_PREFIX = "library.";
+
+    /**
+     * The prefix for attributes that specify additional endorsed libraries to add to
+     * <code>${karaf.home}/lib/endorsed</code>
+     */
+    String ENDORSED_PREFIX = "endorsed.";
+
+    /**
+     * The prefix for attributes that specify additional extension libraries to add to
+     * <code>${karaf.home}/lib/ext</code>
+     */
+    String EXT_PREFIX = "ext.";
+
+    /**
+     * The prefix for attributes that specify additional endorsed libraries to add to
+     * <code>${karaf.home}/lib/boot</code>
+     */
+    String BOOT_PREFIX = "boot.";
+
+    /**
+     * The prefix for attributes that specify bundle overrides
+     * (see {@link org.apache.karaf.features.internal.service.Overrides}). In version 4.2 it's better to use
+     * {@link org.apache.karaf.features.internal.service.FeaturesProcessor} configuration.
+     */
+    String OVERRIDE_PREFIX = "override.";
+
+    /**
+     * The prefix for attributes that specify optional resources
+     */
+    String OPTIONAL_PREFIX = "optional.";
+
+}
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 f14c6ae..6df4243 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
@@ -39,6 +39,7 @@ import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
@@ -48,14 +49,17 @@ import java.util.concurrent.ScheduledExecutorService;
 import java.util.function.Function;
 import java.util.jar.Attributes;
 import java.util.jar.Manifest;
+import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
 import org.apache.felix.resolver.ResolverImpl;
 import org.apache.felix.utils.manifest.Clause;
 import org.apache.felix.utils.properties.Properties;
+import org.apache.karaf.features.FeaturePattern;
 import org.apache.karaf.features.FeaturesService;
 import org.apache.karaf.features.Library;
+import org.apache.karaf.features.LocationPattern;
 import org.apache.karaf.features.internal.download.DownloadCallback;
 import org.apache.karaf.features.internal.download.DownloadManager;
 import org.apache.karaf.features.internal.download.Downloader;
@@ -67,17 +71,21 @@ import org.apache.karaf.features.internal.model.Dependency;
 import org.apache.karaf.features.internal.model.Feature;
 import org.apache.karaf.features.internal.model.Features;
 import org.apache.karaf.features.internal.model.JaxbUtil;
+import org.apache.karaf.features.internal.model.processing.FeaturesProcessing;
 import org.apache.karaf.features.internal.repository.BaseRepository;
 import org.apache.karaf.features.internal.resolver.ResourceBuilder;
 import org.apache.karaf.features.internal.service.Blacklist;
 import org.apache.karaf.features.internal.service.Deployer;
 import org.apache.karaf.features.internal.util.MapUtils;
+import org.apache.karaf.features.internal.util.MultiException;
 import org.apache.karaf.kar.internal.Kar;
 import org.apache.karaf.profile.Profile;
 import org.apache.karaf.profile.ProfileBuilder;
 import org.apache.karaf.profile.impl.Profiles;
 import org.apache.karaf.tools.utils.KarafPropertiesEditor;
 import org.apache.karaf.tools.utils.model.KarafPropertyEdits;
+import org.apache.karaf.util.ThreadUtils;
+import org.apache.karaf.util.Version;
 import org.apache.karaf.util.config.PropertiesLoader;
 import org.apache.karaf.util.maven.Parser;
 import org.ops4j.pax.url.mvn.MavenResolver;
@@ -91,9 +99,11 @@ import org.slf4j.LoggerFactory;
 
 import static java.util.Collections.singletonList;
 import static java.util.jar.JarFile.MANIFEST_NAME;
-import static org.apache.karaf.features.internal.service.Blacklist.TYPE_REPOSITORY;
 import static org.apache.karaf.profile.assembly.Builder.Stage.Startup;
 
+/**
+ * A builder-like class to create instances of {@link Profile profiles}.
+ */
 public class Builder {
 
     private static final String STATIC_FEATURES_KAR = "mvn:org.apache.karaf.features/static/%s/kar";
@@ -106,25 +116,105 @@ public class Builder {
     private static final String LIBRARY_CLAUSE_TYPE = "type";
     private static final String LIBRARY_CLAUSE_EXPORT = "export";
     private static final String LIBRARY_CLAUSE_DELEGATE = "delegate";
+
     public static final String ORG_OPS4J_PAX_URL_MVN_PID = "org.ops4j.pax.url.mvn";
 
+    /**
+     * <p>An indication of <em>stage</em> for bundles/features/repositories/kars/profiles.</p>
+     */
     public enum Stage {
-        Startup, Boot, Installed
+        /**
+         * Karaf runtime is in <em>startup</em> stage when it installs OSGi bundles into OSGi framework before
+         * passing this responsibility to {@link FeaturesService}. A list of bundles to install is defined
+         * in <code>${karaf.etc}/startup.properties</code>.
+         */
+        Startup,
+        /**
+         * Karaf runtime is in <em>boot</em> stage when it installs OSGi bundles using Karaf features. Features
+         * (and features XML repositories) are defined in <code>${karaf.etc}/org.apache.karaf.features.cfg</code>.
+         * Repositories and features available in startup stage should be <em>visible</em> in boot stage as well, as
+         * this is the stage where term <em>Karaf feature</em> gets its meaning.
+         */
+        Boot,
+        /**
+         * <em>Installed</em> stage is just a space where bundles and features may be installed after starting
+         * Karaf runtime (e.g., using Karaf shell commands, JMX or UI).
+         */
+        Installed;
+
+        /**
+         * Get a {@link Stage} corresponding to Maven scope.
+         * @param scope
+         * @return
+         */
+        public static Stage fromMavenScope(String scope) {
+            switch (scope) {
+                case "compile":
+                    return Builder.Stage.Startup;
+                case "runtime":
+                    return Builder.Stage.Boot;
+                case "provided":
+                    return Builder.Stage.Installed;
+                default:
+                    return null;
+            }
+        }
     }
 
+    /**
+     * <p>An identifiier of Karaf version <em>family</em>. Each version family may have special methods
+     * or requirements for generating/preparing configuration.</p>
+     */
     public enum KarafVersion {
         v24, v3x, v4x
     }
 
+    /**
+     * <p>An idenfifier for supported Java version. This version is used for example in
+     * <code>${karaf.etc}/jre.properties</code> to define system packages for given Java version. Only
+     * supported versions are defined.</p>
+     */
+    public enum JavaVersion {
+        Java16("1.6"), Java17("1.7"), Java18("1.8"), Java9("9");
 
+        private String version;
+
+        JavaVersion(String version) {
+            this.version = version;
+        }
+
+        public static JavaVersion from(String version) {
+            Optional<JavaVersion> v = Arrays.stream(values())
+                    .filter(jv -> jv.version.equals(version))
+                    .findFirst();
+
+            if (!v.isPresent()) {
+                throw new IllegalArgumentException("Java version \"" + version + "\" is not supported");
+            }
+            return v.get();
+        }
+    }
+
+    /**
+     * TODOCUMENT
+     */
     public enum BlacklistPolicy {
         Discard,
         Fail
     }
 
+    /**
+     * Configuration of features XML repository (standalone or inside KAR). <code>addAll</code> may configure
+     * given repository to install all defined features if no explicit feature is specified.
+     */
     static class RepositoryInfo {
         Stage stage;
         boolean addAll;
+
+        public RepositoryInfo(Stage stage, boolean addAll) {
+            this.stage = stage;
+            this.addAll = addAll;
+        }
     }
 
     //
@@ -145,7 +235,7 @@ public class Builder {
     List<String> blacklistedRepositories = new ArrayList<>();
     BlacklistPolicy blacklistPolicy = BlacklistPolicy.Discard;
     List<String> libraries = new ArrayList<>();
-    String javase = "1.8";
+    JavaVersion javase = JavaVersion.Java18;
     KarafVersion karafVersion = KarafVersion.v4x;
     String environment = null;
     boolean useReferenceUrls;
@@ -158,6 +248,7 @@ public class Builder {
     Map<String, String> config = new LinkedHashMap<>();
     Map<String, String> system = new LinkedHashMap<>();
     List<String> pidsToExtract = new LinkedList<>();
+    boolean writeProfiles;
 
     private ScheduledExecutorService executor;
     private DownloadManager manager;
@@ -166,74 +257,139 @@ public class Builder {
     private Path systemDirectory;
     private Map<String, Profile> allProfiles;
     private KarafPropertyEdits propertyEdits;
+    private FeaturesProcessing featuresProcessing = new FeaturesProcessing();
     private Map<String, String> translatedUrls;
 
-    private Function<MavenResolver, MavenResolver> resolverWrapper = null;
+    private Function<MavenResolver, MavenResolver> resolverWrapper = Function.identity();
 
     public static Builder newInstance() {
         return new Builder();
     }
 
+    /**
+     * Sets the {@link Stage} used by next builder invocations.
+     * @param stage
+     * @return
+     */
     public Builder defaultStage(Stage stage) {
         this.defaultStage = stage;
         return this;
     }
 
+    /**
+     * Sets default <em>add all</em> flag for KARs and repositories.
+     * @param addAll
+     * @return
+     */
     public Builder defaultAddAll(boolean addAll) {
         this.defaultAddAll = addAll;
         return this;
     }
 
+    /**
+     * Configure a list of profile URIs to be used for profile import
+     * @param profilesUri
+     * @return
+     */
     public Builder profilesUris(String... profilesUri) {
         Collections.addAll(this.profilesUris, profilesUri);
         return this;
     }
 
+    /**
+     * Configure libraries to use. Each library may contain OSGi header-like directives: <code>type</code>,
+     * <code>url</code>, <code>export</code> and <code>delegate</code>.
+     * @param libraries
+     * @return
+     */
     public Builder libraries(String... libraries) {
         Collections.addAll(this.libraries, libraries);
         return this;
     }
 
+    /**
+     * Configure KARs to use at current {@link #defaultStage stage} with default <em>add all</em> flag
+     * @param kars
+     * @return
+     */
     public Builder kars(String... kars) {
         return kars(defaultStage, defaultAddAll, kars);
     }
 
+    /**
+     * Configure KARs to use at current {@link #defaultStage stage} with given <em>add all</em> flag
+     * @param addAll
+     * @param kars
+     * @return
+     */
     public Builder kars(boolean addAll, String... kars) {
         return kars(defaultStage, addAll, kars);
     }
 
+    /**
+     * Configure KARs to use at given stage with given <em>add all</em> flag
+     * @param stage
+     * @param addAll
+     * @param kars
+     * @return
+     */
     public Builder kars(Stage stage, boolean addAll, String... kars) {
         for (String kar : kars) {
-            RepositoryInfo info = new RepositoryInfo();
-            info.stage = stage;
-            info.addAll = addAll;
-            this.kars.put(kar, info);
+            this.kars.put(kar, new RepositoryInfo(stage, addAll));
         }
         return this;
     }
 
+    /**
+     * Configure features XML repositories to use at current {@link #defaultStage stage} with default <em>add all</em> flag
+     * @param repositories
+     * @return
+     */
     public Builder repositories(String... repositories) {
         return repositories(defaultStage, defaultAddAll, repositories);
     }
 
+    /**
+     * Configure features XML repositories to use at current {@link #defaultStage stage} with given <em>add all</em> flag
+     * @param addAll
+     * @param repositories
+     * @return
+     */
     public Builder repositories(boolean addAll, String... repositories) {
         return repositories(defaultStage, addAll, repositories);
     }
 
+    /**
+     * Configure features XML repositories to use at given stage with given <em>add all</em> flag
+     * @param stage
+     * @param addAll
+     * @param repositories
+     * @return
+     */
     public Builder repositories(Stage stage, boolean addAll, String... repositories) {
         for (String repository : repositories) {
-            RepositoryInfo info = new RepositoryInfo();
-            info.stage = stage;
-            info.addAll = addAll;
-            this.repositories.put(repository, info);
+            this.repositories.put(repository, new RepositoryInfo(stage, addAll));
         }
         return this;
     }
 
+    /**
+     * Configure features to use at current {@link #defaultStage stage}. Each feature may be specified as
+     * <code>name</code> or <code>name/version</code> (no version ranges allowed).
+     * @param features
+     * @return
+     */
     public Builder features(String... features) {
         return features(defaultStage, features);
     }
 
+    /**
+     * Configure features to use at given stage. Each feature may be specified as <code>name</code> or
+     * <code>name/version</code> (no version ranges allowed).
+     * @param stage
+     * @param features
+     * @return
+     */
     public Builder features(Stage stage, String... features) {
         for (String feature : features) {
             this.features.put(feature, stage);
@@ -241,10 +397,21 @@ public class Builder {
         return this;
     }
 
+    /**
+     * Configure bundle URIs to use at current {@link #defaultStage stage}.
+     * @param bundles
+     * @return
+     */
     public Builder bundles(String... bundles) {
         return bundles(defaultStage, bundles);
     }
 
+    /**
+     * Configure bundle URIs to use at given stage.
+     * @param stage
+     * @param bundles
+     * @return
+     */
     public Builder bundles(Stage stage, String... bundles) {
         for (String bundle : bundles) {
             this.bundles.put(bundle, stage);
@@ -252,10 +419,21 @@ public class Builder {
         return this;
     }
 
+    /**
+     * Configure profiles to use at current {@link #defaultStage stage}.
+     * @param profiles
+     * @return
+     */
     public Builder profiles(String... profiles) {
         return profiles(defaultStage, profiles);
     }
 
+    /**
+     * Configure profiles to use at given stage.
+     * @param stage
+     * @param profiles
+     * @return
+     */
     public Builder profiles(Stage stage, String... profiles) {
         for (String profile : profiles) {
             this.profiles.put(profile, stage);
@@ -263,6 +441,11 @@ public class Builder {
         return this;
     }
 
+    /**
+     * Configure target directory, where distribution is being assembled.
+     * @param homeDirectory
+     * @return
+     */
     public Builder homeDirectory(Path homeDirectory) {
         if (homeDirectory == null) {
             throw new IllegalArgumentException("homeDirectory is null");
@@ -271,101 +454,213 @@ public class Builder {
         return this;
     }
 
+    /**
+     * Configure Java version to use. This version will be resolved in several property placeholders inside
+     * <code>${karaf.etc}/config.properties</code> and <code>${karaf.etc}/jre.properties</code>.
+     * @param javase
+     * @return
+     */
     public Builder javase(String javase) {
         if (javase == null) {
             throw new IllegalArgumentException("javase is null");
         }
-        this.javase = javase;
+        this.javase = JavaVersion.from(javase);
         return this;
     }
 
+    /**
+     * Set environment to use that may be used to select different variant of PID configuration file, e.g.,
+     * <code>org.ops4j.pax.url.mvn.cfg#docker</code>.
+     * @param environment
+     * @return
+     */
     public Builder environment(String environment) {
         this.environment = environment;
         return this;
     }
 
+    /**
+     * Configure builder to generate <code>reference:</code>-like URIs in <code>${karaf.etc}/startup.properties</code>.
+     * Bundles declared in this way are not copied (by Felix) to <code>data/cache</code> directory, but are
+     * used from original location.
+     * @return
+     */
     public Builder useReferenceUrls() {
         return useReferenceUrls(true);
     }
 
+    /**
+     * Configure builder to use (when <code>true</code>) <code>reference:</code>-like URIs in
+     * <code>${karaf.etc}/startup.properties</code>.
+     * @param useReferenceUrls
+     * @return
+     */
     public Builder useReferenceUrls(boolean useReferenceUrls) {
         this.useReferenceUrls = useReferenceUrls;
         return this;
     }
 
+    /**
+     * Configure builder to copy generated and configured profiles into <code>${karaf.etc}/profiles</code>
+     * directory.
+     * @param writeProfiles
+     */
+    public void writeProfiles(boolean writeProfiles) {
+        this.writeProfiles = writeProfiles;
+    }
+
+    /**
+     * Configure Karaf version to target. This impacts the way some configuration files are generated.
+     * @param karafVersion
+     * @return
+     */
     public Builder karafVersion(KarafVersion karafVersion) {
         this.karafVersion = karafVersion;
         return this;
     }
 
+    /**
+     * Sets default start level for bundles declared in <code>${karaf.etc}/startup.properties</code>.
+     * @param defaultStartLevel
+     * @return
+     */
     public Builder defaultStartLevel(int defaultStartLevel) {
         this.defaultStartLevel = defaultStartLevel;
         return this;
     }
 
+    /**
+     * Ignore the dependency attribute (dependency="[true|false]") on bundles, effectively forcing their
+     * installation.
+     */
     public Builder ignoreDependencyFlag() {
         return ignoreDependencyFlag(true);
     }
 
+    /**
+     * Configures builder to ignore (or not) <code>dependency</code> flag on bundles declared
+     * in features XML file.
+     * @param ignoreDependencyFlag
+     * @return
+     */
     public Builder ignoreDependencyFlag(boolean ignoreDependencyFlag) {
         this.ignoreDependencyFlag = ignoreDependencyFlag;
         return this;
     }
 
+    /**
+     * Configures builder to use offline pax-url-aether resolver
+     * @return
+     */
+    public Builder offline() {
+        return offline(true);
+    }
+
+    /**
+     * Configures whether pax-url-aether resolver should work in offline mode
+     * @param offline
+     * @return
+     */
     public Builder offline(boolean offline) {
         this.offline = offline;
         return this;
     }
 
-    public Builder offline() {
-        return offline(true);
-    }
-
+    /**
+     * Configures local Maven repository to use by pax-url-aether. By default, assembly mojo sets the value
+     * read from current Maven build.
+     * @param localRepository
+     * @return
+     */
     public Builder localRepository(String localRepository) {
         this.localRepository = localRepository;
         return this;
     }
 
+    /**
+     * Configures comma-separated list of remote Maven repositories to use by pax-url-aether.
+     * By default, assembly mojo sets the repositories from current Maven build.
+     * @param mavenRepositories
+     * @return
+     */
     public Builder mavenRepositories(String mavenRepositories) {
         this.mavenRepositories = mavenRepositories;
         return this;
     }
 
+    /**
+     * Configures a function that may alter/replace {@link MavenResolver} used to resolve <code>mvn:</code> URIs.
+     * @param wrapper
+     * @return
+     */
     public Builder resolverWrapper(Function<MavenResolver, MavenResolver> wrapper) {
         this.resolverWrapper = wrapper;
         return this;
     }
 
+    /**
+     * Short-hand builder configuration to use standard Karaf static KAR at current Karaf version
+     * @return
+     */
     public Builder staticFramework() {
-        // TODO: load this from resources
-        return staticFramework("4.0.0-SNAPSHOT");
+        return staticFramework(Version.karafVersion());
     }
 
+    /**
+     * Short-hand builder configuration to use standard Karaf static KAR at given Karaf version
+     * @param version
+     * @return
+     */
     public Builder staticFramework(String version) {
         String staticFeaturesKar = String.format(STATIC_FEATURES_KAR, version);
         return this.defaultStage(Startup).useReferenceUrls().kars(Startup, true, staticFeaturesKar);
     }
 
+    /**
+     * Configure a list of blacklisted profile names (possibly using <code>*</code> glob)
+     * @param profiles
+     * @return
+     */
     public Builder blacklistProfiles(Collection<String> profiles) {
         this.blacklistedProfiles.addAll(profiles);
         return this;
     }
 
+    /**
+     * Configure a list of blacklisted feature names (see {@link FeaturePattern})
+     * @param features
+     * @return
+     */
     public Builder blacklistFeatures(Collection<String> features) {
         this.blacklistedFeatures.addAll(features);
         return this;
     }
 
+    /**
+     * Configure a list of blacklisted bundle URIs (see {@link LocationPattern})
+     * @param bundles
+     * @return
+     */
     public Builder blacklistBundles(Collection<String> bundles) {
         this.blacklistedBundles.addAll(bundles);
         return this;
     }
 
+    /**
+     * Configure a list of blacklisted features XML repository URIs (see {@link LocationPattern})
+     * @param repositories
+     * @return
+     */
     public Builder blacklistRepositories(Collection<String> repositories) {
         this.blacklistedRepositories.addAll(repositories);
         return this;
     }
 
+    /**
+     * TODOCUMENT
+     * @param policy
+     * @return
+     */
     public Builder blacklistPolicy(BlacklistPolicy policy) {
         this.blacklistPolicy = policy;
         return this;
@@ -381,6 +676,12 @@ public class Builder {
         return this;
     }
 
+    /**
+     * Configures a list of PIDs (or PID patterns) to copy to <code>${karaf.etc}</code> from features, when
+     * assembling a distribution
+     * @param pidsToExtract
+     * @return
+     */
     public Builder pidsToExtract(List<String> pidsToExtract) {
         if (pidsToExtract != null) {
             for (String pid : pidsToExtract) {
@@ -401,11 +702,23 @@ public class Builder {
         return this;
     }
 
+    /**
+     * Configures additional properties to add to <code>${karaf.etc}/config.properties</code>
+     * @param key
+     * @param value
+     * @return
+     */
     public Builder config(String key, String value) {
         this.config.put(key, value);
         return this;
     }
 
+    /**
+     * Configures additional properties to add to <code>${karaf.etc}/system.properties</code>
+     * @param key
+     * @param value
+     * @return
+     */
     public Builder system(String key, String value) {
         this.system.put(key, value);
         return this;
@@ -435,6 +748,10 @@ public class Builder {
         return pidsToExtract;
     }
 
+    /**
+     * Main method to generate custom Karaf distribution using configuration provided with builder-like methods.
+     * @throws Exception
+     */
     public void generateAssembly() throws Exception {
         if (javase == null) {
             throw new IllegalArgumentException("javase is not set");
@@ -443,6 +760,11 @@ public class Builder {
             throw new IllegalArgumentException("homeDirectory is not set");
         }
         try {
+            executor = Executors.newScheduledThreadPool(8, ThreadUtils.namedThreadFactory("builder"));
+
+            systemDirectory = homeDirectory.resolve("system");
+            etcDirectory = homeDirectory.resolve("etc");
+
             doGenerateAssembly();
         } finally {
             if (executor != null) {
@@ -452,47 +774,35 @@ public class Builder {
     }
 
     private void doGenerateAssembly() throws Exception {
-        systemDirectory = homeDirectory.resolve("system");
-        etcDirectory = homeDirectory.resolve("etc");
-
-        LOGGER.info("Generating karaf assembly: " + homeDirectory);
+        LOGGER.info("Generating Karaf assembly: " + homeDirectory);
 
         //
-        // Create download manager
+        // Create download manager - combination of pax-url-aether and a resolver wrapper that may
+        // alter the way pax-url-aether resolver works
         //
-        Dictionary<String, String> props = new Hashtable<>();
-        if (offline) {
-            props.put(ORG_OPS4J_PAX_URL_MVN_PID + "offline", "true");
-        }
-        if (localRepository != null) {
-            props.put(Builder.ORG_OPS4J_PAX_URL_MVN_PID + ".localRepository", localRepository);
-        }
-        if (mavenRepositories != null) {
-            props.put(Builder.ORG_OPS4J_PAX_URL_MVN_PID + ".repositories", mavenRepositories);
-        }
-        MavenResolver resolver = MavenResolvers.createMavenResolver(props, ORG_OPS4J_PAX_URL_MVN_PID);
-        if (resolverWrapper != null) {
-            resolver = resolverWrapper.apply(resolver);
-        }
-        executor = Executors.newScheduledThreadPool(8);
+        MavenResolver resolver = createMavenResolver();
         manager = new CustomDownloadManager(resolver, executor, null, translatedUrls);
         this.resolver = new ResolverImpl(new Slf4jResolverLog(LOGGER));
 
         //
-        // Unzip kars
+        // Unzip KARs
         //
         LOGGER.info("Unzipping kars");
-        Map<String, RepositoryInfo> repositories = new LinkedHashMap<>(this.repositories);
+//        Map<String, RepositoryInfo> repositories = new LinkedHashMap<>(this.repositories);
         Downloader downloader = manager.createDownloader();
         for (String kar : kars.keySet()) {
             downloader.download(kar, null);
         }
         downloader.await();
+        // each KAR is extracted and all features XML repositories found there are added to the same
+        // stage as the KAR and with the same "add all" flag as the KAR itself
         for (String karUri : kars.keySet()) {
+            LOGGER.info("   processing KAR: " + karUri);
             Kar kar = new Kar(manager.getProviders().get(karUri).getFile().toURI());
             kar.extract(systemDirectory.toFile(), homeDirectory.toFile());
             RepositoryInfo info = kars.get(karUri);
             for (URI repositoryUri : kar.getFeatureRepos()) {
+                LOGGER.info("      found repository: " + repositoryUri);
                 repositories.put(repositoryUri.toString(), info);
             }
         }
@@ -500,12 +810,13 @@ public class Builder {
         //
         // Propagate feature installation from repositories
         //
-        LOGGER.info("   Loading repositories");
-        Map<String, Stage> features = new LinkedHashMap<>(this.features);
+        LOGGER.info("Loading repositories");
+//        Map<String, Stage> features = new LinkedHashMap<>(this.features);
         Map<String, Features> karRepositories = loadRepositories(manager, repositories.keySet(), false);
         for (String repo : repositories.keySet()) {
             RepositoryInfo info = repositories.get(repo);
             if (info.addAll) {
+                LOGGER.info("   adding all features from repository: " + repo + " (stage: " + info.stage + ")");
                 for (Feature feature : karRepositories.get(repo).getFeature()) {
                     features.put(feature.getId(), info.stage);
                 }
@@ -515,72 +826,74 @@ public class Builder {
         //
         // Load profiles
         //
-        LOGGER.info("Loading profiles");
-        allProfiles = new HashMap<>();
-        for (String profilesUri : profilesUris) {
-            String uri = profilesUri;
-            if (uri.startsWith("jar:") && uri.contains("!/")) {
-                uri = uri.substring("jar:".length(), uri.indexOf("!/"));
-            }
-            if (!uri.startsWith("file:")) {
-                downloader = manager.createDownloader();
-                downloader.download(uri, null);
-                downloader.await();
-                StreamProvider provider = manager.getProviders().get(uri);
-                profilesUri = profilesUri.replace(uri, provider.getFile().toURI().toString());
-            }
-            URI profileURI = URI.create(profilesUri);
-            Path profilePath;
-            try {
-                profilePath = Paths.get(profileURI);
-            } catch (FileSystemNotFoundException e) {
-                // file system does not exist, try to create it
-                FileSystem fs = FileSystems.newFileSystem(profileURI, new HashMap<>(), Builder.class.getClassLoader());
-                profilePath = fs.provider().getPath(profileURI);
-            }
-            allProfiles.putAll(Profiles.loadProfiles(profilePath));
-            // Handle blacklisted profiles
-            if (!blacklistedProfiles.isEmpty()) {
-                if (blacklistPolicy == BlacklistPolicy.Discard) {
-                    // Override blacklisted profiles with empty ones
-                    for (String profile : blacklistedProfiles) {
-                        allProfiles.put(profile, ProfileBuilder.Factory.create(profile).getProfile());
-                    }
-                } else {
-                    // Remove profiles completely
-                    allProfiles.keySet().removeAll(blacklistedProfiles);
-                }
-            }
+        LOGGER.info("Loading profiles from:");
+        profilesUris.forEach(p -> LOGGER.info("   " + p));
+        allProfiles = loadExternalProfiles(profilesUris);
+        if (allProfiles.size() > 0) {
+            StringBuilder sb = new StringBuilder();
+            LOGGER.info("   Found profiles: " + allProfiles.keySet().stream().collect(Collectors.joining(", ")));
         }
 
-        // Generate profiles
+        //
+        // Generate profiles. If user has configured additional profiles, they'll be used as parents
+        // of the generated ones.
+        //
         Profile startupProfile = generateProfile(Stage.Startup, profiles, repositories, features, bundles);
+        allProfiles.put(startupProfile.getId(), startupProfile);
+
+        // generated startup profile should be used (together with configured startup and boot profiles) as parent
+        // of the generated boot profile - similar visibility rule (boot stage requires startup stage) is applied
+        // for repositories and features
         profiles.put(startupProfile.getId(), Stage.Boot);
         Profile bootProfile = generateProfile(Stage.Boot, profiles, repositories, features, bundles);
+        allProfiles.put(bootProfile.getId(), bootProfile);
+
         Profile installedProfile = generateProfile(Stage.Installed, profiles, repositories, features, bundles);
+        allProfiles.put(installedProfile.getId(), installedProfile);
 
         //
-        // Compute overall profile
+        // Compute "overlay" profile - a single profile with all parent profiles included (when there's the same
+        // file in both profiles, parent profile's version has lower priority)
         //
         ProfileBuilder builder = ProfileBuilder.Factory.create(UUID.randomUUID().toString())
                 .setParents(Arrays.asList(startupProfile.getId(), bootProfile.getId(), installedProfile.getId()));
         config.forEach((k ,v) -> builder.addConfiguration(Profile.INTERNAL_PID, Profile.CONFIG_PREFIX + k, v));
         system.forEach((k ,v) -> builder.addConfiguration(Profile.INTERNAL_PID, Profile.SYSTEM_PREFIX + k, v));
-        Profile overallProfile = builder
-                .getProfile();
+        // profile with all the parents configured
+        Profile overallProfile = builder.getProfile();
+
+        // profile with parents included and "flattened" using inheritance rules (child files overwrite parent
+        // files and child PIDs are merged with parent PIDs and same properties are taken from child profiles)
         Profile overallOverlay = Profiles.getOverlay(overallProfile, allProfiles, environment);
+
+        // profile with property placeholders resolved or left unchanged (if there's no property value available,
+        // so property placeholders are preserved - like ${karaf.base})
         Profile overallEffective = Profiles.getEffective(overallOverlay, false);
 
+        if (writeProfiles) {
+            Path profiles = etcDirectory.resolve("profiles");
+            LOGGER.info("Adding profiles to {}", homeDirectory.relativize(profiles));
+            allProfiles.forEach((id, profile) -> {
+                try {
+                    Profiles.writeProfile(profiles, profile);
+                } catch (IOException e) {
+                    LOGGER.warn("Problem writing profile {}: {}", id, e.getMessage());
+                }
+            });
+        }
+
         manager = new CustomDownloadManager(resolver, executor, overallEffective, translatedUrls);
 
-//        Hashtable<String, String> agentProps = new Hashtable<>(overallEffective.getConfiguration(ORG_OPS4J_PAX_URL_MVN_PID));
+//        Hashtable<String, String> profileProps = new Hashtable<>(overallEffective.getConfiguration(ORG_OPS4J_PAX_URL_MVN_PID));
 //        final Map<String, String> properties = new HashMap<>();
 //        properties.put("karaf.default.repository", "system");
-//        InterpolationHelper.performSubstitution(agentProps, properties::get, false, false, true);
+//        InterpolationHelper.performSubstitution(profileProps, properties::get, false, false, true);
 
         //
         // Write config and system properties
         //
+        LOGGER.info("Configuring etc/config.properties and etc/system.properties");
+
         Path configPropertiesPath = etcDirectory.resolve("config.properties");
         Properties configProperties = new Properties(configPropertiesPath.toFile());
         configProperties.putAll(overallEffective.getConfig());
@@ -595,11 +908,13 @@ public class Builder {
         // Download libraries
         //
         // TODO: handle karaf 2.x and 3.x libraries
-        LOGGER.info("Downloading libraries");
         downloader = manager.createDownloader();
+        LOGGER.info("Downloading libraries for generated profiles");
         downloadLibraries(downloader, configProperties, overallEffective.getLibraries(), "");
+        LOGGER.info("Downloading additional libraries");
         downloadLibraries(downloader, configProperties, libraries, "");
         downloader.await();
+
         // Reformat clauses
         reformatClauses(configProperties, Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA);
         reformatClauses(configProperties, Constants.FRAMEWORK_BOOTDELEGATION);
@@ -678,6 +993,65 @@ public class Builder {
         installStage(installedProfile, allBootFeatures);
     }
 
+    private MavenResolver createMavenResolver() {
+        Dictionary<String, String> props = new Hashtable<>();
+        if (offline) {
+            props.put(ORG_OPS4J_PAX_URL_MVN_PID + "offline", "true");
+        }
+        if (localRepository != null) {
+            props.put(ORG_OPS4J_PAX_URL_MVN_PID + ".localRepository", localRepository);
+        }
+        if (mavenRepositories != null) {
+            props.put(ORG_OPS4J_PAX_URL_MVN_PID + ".repositories", mavenRepositories);
+        }
+        MavenResolver resolver = MavenResolvers.createMavenResolver(props, ORG_OPS4J_PAX_URL_MVN_PID);
+        return resolverWrapper.apply(resolver);
+    }
+
+    /**
+     * Loads all profiles declared in profile URIs. These will be used in addition to generated
+     * <em>startup</em>, <em>boot</em> and <em>installed</em> profiles.
+     */
+    private Map<String, Profile> loadExternalProfiles(List<String> profilesUris) throws IOException, MultiException, InterruptedException {
+        Map<String, Profile> profiles = new LinkedHashMap<>();
+        for (String profilesUri : profilesUris) {
+            String uri = profilesUri;
+            if (uri.startsWith("jar:") && uri.contains("!/")) {
+                uri = uri.substring("jar:".length(), uri.indexOf("!/"));
+            }
+            if (!uri.startsWith("file:")) {
+                Downloader downloader = manager.createDownloader();
+                downloader.download(uri, null);
+                downloader.await();
+                StreamProvider provider = manager.getProviders().get(uri);
+                profilesUri = profilesUri.replace(uri, provider.getFile().toURI().toString());
+            }
+            URI profileURI = URI.create(profilesUri);
+            Path profilePath;
+            try {
+                profilePath = Paths.get(profileURI);
+            } catch (FileSystemNotFoundException e) {
+                // file system does not exist, try to create it
+                FileSystem fs = FileSystems.newFileSystem(profileURI, new HashMap<>(), Builder.class.getClassLoader());
+                profilePath = fs.provider().getPath(profileURI);
+            }
+            profiles.putAll(Profiles.loadProfiles(profilePath));
+            // Handle blacklisted profiles
+            if (!blacklistedProfiles.isEmpty()) {
+                if (blacklistPolicy == BlacklistPolicy.Discard) {
+                    // Override blacklisted profiles with empty ones
+                    for (String profile : blacklistedProfiles) {
+                        profiles.put(profile, ProfileBuilder.Factory.create(profile).getProfile());
+                    }
+                } else {
+                    // Remove profiles completely
+                    profiles.keySet().removeAll(blacklistedProfiles);
+                }
+            }
+        }
+        return profiles;
+    }
+
     private void reformatClauses(Properties config, String key) {
         String val = config.getProperty(key);
         if (val != null && !val.isEmpty()) {
@@ -902,8 +1276,8 @@ public class Builder {
             Map<String, List<String>> prereqs = new HashMap<>();
             prereqs.put("blueprint:", Arrays.asList("deployer", "aries-blueprint"));
             prereqs.put("spring:", Arrays.asList("deployer", "spring"));
-            prereqs.put("wrap:", Arrays.asList("wrap"));
-            prereqs.put("war:", Arrays.asList("war"));
+            prereqs.put("wrap:", Collections.singletonList("wrap"));
+            prereqs.put("war:", Collections.singletonList("war"));
             ArtifactInstaller installer = new ArtifactInstaller(systemDirectory, downloader, blacklistedBundles);
             for (String location : locations) {
                 installer.installArtifact(location);
@@ -1104,6 +1478,12 @@ public class Builder {
         return startupEffective;
     }
 
+    /**
+     * Gets a list of objects (bundle URIs, profile IDs, feature IDs) configured for given stage
+     * @param stage
+     * @param data
+     * @return
+     */
     private List<String> getStaged(Stage stage, Map<String, Stage> data) {
         List<String> staged = new ArrayList<>();
         for (String s : data.keySet()) {
@@ -1114,6 +1494,13 @@ public class Builder {
         return staged;
     }
 
+    /**
+     * Gets a list of features XML repository URIs configured for given stage. There's one special rule - startup
+     * repositories are added as boot repositories as well.
+     * @param stage
+     * @param data
+     * @return
+     */
     private List<String> getStagedRepositories(Stage stage, Map<String, RepositoryInfo> data) {
         List<String> staged = new ArrayList<>();
         for (String s : data.keySet()) {
@@ -1132,8 +1519,7 @@ public class Builder {
         final List<String> blacklist = new ArrayList<>();
         blacklist.addAll(blacklistedBundles);
         blacklist.addAll(blacklistedFeatures);
-        final List<String> blacklistRepos = new ArrayList<>();
-        blacklistRepos.addAll(blacklistedRepositories);
+        final List<String> blacklistRepos = new ArrayList<>(blacklistedRepositories);
         final Blacklist blacklistOther = new Blacklist(blacklist);
         final Blacklist repoBlacklist = new Blacklist(blacklistRepos);
         for (String repository : repositories) {
@@ -1142,7 +1528,7 @@ public class Builder {
                 public void downloaded(final StreamProvider provider) throws Exception {
                     String url = provider.getUrl();
                     if (repoBlacklist.isRepositoryBlacklisted(url)) {
-                        LOGGER.info("      feature repository " + url + " is blacklisted");
+                        LOGGER.info("   feature repository " + url + " is blacklisted");
                         return;
                     }
                     synchronized (loaded) {
@@ -1174,15 +1560,31 @@ public class Builder {
         return loaded;
     }
 
-    private Profile generateProfile(Stage stage, Map<String, Stage> profiles, Map<String, RepositoryInfo> repositories, Map<String, Stage> features, Map<String, Stage> bundles) {
-        Profile profile = ProfileBuilder.Factory.create(UUID.randomUUID().toString())
-                .setParents(getStaged(stage, profiles))
+    /**
+     * Generate internal profile (for the purpose of custom assembly builder) for given <code>stage</code>.
+     * @param stage a {@link Stage} for which the profile is being generated
+     * @param parentProfiles all profiles for given stage will be used as parent profiles
+     * @param repositories repositories to use in generated profile
+     * @param features features to declare in generated profile
+     * @param bundles bundles to declare in generated profile
+     * @return
+     */
+    private Profile generateProfile(Stage stage, Map<String, Stage> parentProfiles, Map<String, RepositoryInfo> repositories, Map<String, Stage> features, Map<String, Stage> bundles) {
+        String name = "generated-" + stage.name().toLowerCase();
+        List<String> stagedParentProfiles = getStaged(stage, parentProfiles);
+
+        if (stagedParentProfiles.isEmpty()) {
+            LOGGER.info("Generating {} profile", name);
+        } else {
+            LOGGER.info("Generating {} profile with parents: {}", name, stagedParentProfiles.stream().collect(Collectors.joining(", ")));
+        }
+
+        return ProfileBuilder.Factory.create(name)
+                .setParents(stagedParentProfiles)
                 .setRepositories(getStagedRepositories(stage, repositories))
                 .setFeatures(getStaged(stage, features))
                 .setBundles(getStaged(stage, bundles))
                 .getProfile();
-        allProfiles.put(profile.getId(), profile);
-        return profile;
     }
 
     private Map<String, Integer> resolve(
@@ -1254,7 +1656,7 @@ public class Builder {
     private BundleRevision getSystemBundle() throws Exception {
         Path configPropPath = etcDirectory.resolve("config.properties");
         Properties configProps = PropertiesLoader.loadPropertiesOrFail(configPropPath.toFile());
-        configProps.put("java.specification.version", javase);
+        configProps.put("java.specification.version", javase.version);
         configProps.substitute();
 
         Attributes attributes = new Attributes();
@@ -1262,14 +1664,18 @@ public class Builder {
         attributes.putValue(Constants.BUNDLE_SYMBOLICNAME, "system.bundle");
         attributes.putValue(Constants.BUNDLE_VERSION, "0.0.0");
 
-        String exportPackages = configProps.getProperty(Constants.FRAMEWORK_SYSTEMPACKAGES);
+        String exportPackages = configProps.getProperty(Constants.FRAMEWORK_SYSTEMPACKAGES, "");
+        if ("".equals(exportPackages.trim())) {
+            throw new IllegalArgumentException("\"org.osgi.framework.system.packages\" property should specify system bundle" +
+                    " packages. It can't be empty, please check etc/config.properties of the assembly.");
+        }
         if (configProps.containsKey(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA)) {
             exportPackages += "," + configProps.getProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA);
         }
         exportPackages = exportPackages.replaceAll(",\\s*,", ",");
         attributes.putValue(Constants.EXPORT_PACKAGE, exportPackages);
 
-        String systemCaps = configProps.getProperty(Constants.FRAMEWORK_SYSTEMCAPABILITIES);
+        String systemCaps = configProps.getProperty(Constants.FRAMEWORK_SYSTEMCAPABILITIES, "");
         attributes.putValue(Constants.PROVIDE_CAPABILITY, systemCaps);
 
         final Hashtable<String, String> headers = new Hashtable<>();
diff --git a/profile/src/main/java/org/apache/karaf/profile/command/ProfileDisplay.java b/profile/src/main/java/org/apache/karaf/profile/command/ProfileDisplay.java
index e6d4619..d92bf8a 100644
--- a/profile/src/main/java/org/apache/karaf/profile/command/ProfileDisplay.java
+++ b/profile/src/main/java/org/apache/karaf/profile/command/ProfileDisplay.java
@@ -54,7 +54,7 @@ public class ProfileDisplay implements Action {
     private ProfileService profileService;
 
     @Override
-    public Object execute() throws Exception {
+    public Object execute() {
         displayProfile(profileService.getRequiredProfile(profileId));
         return null;
     }
@@ -87,11 +87,11 @@ public class ProfileDisplay implements Action {
 
         Map<String, Map<String, Object>> configuration = new HashMap<>(profile.getConfigurations());
         Map<String, byte[]> resources = profile.getFileConfigurations();
-        Map<String,Object> agentConfiguration = profile.getConfiguration(Profile.INTERNAL_PID);
-        List<String> agentProperties = new ArrayList<>();
+        Map<String,Object> profileConfiguration = profile.getConfiguration(Profile.INTERNAL_PID);
+        List<String> profileProperties = new ArrayList<>();
         List<String> systemProperties = new ArrayList<>();
         List<String> configProperties = new ArrayList<>();
-        for (Map.Entry<String, Object> entry : agentConfiguration.entrySet()) {
+        for (Map.Entry<String, Object> entry : profileConfiguration.entrySet()) {
             String key = entry.getKey();
             Object value = entry.getValue();
             if (value instanceof String && ((String) value).contains(",")) {
@@ -107,7 +107,7 @@ public class ProfileDisplay implements Action {
             else if (!key.startsWith("feature.") && !key.startsWith("repository") &&
                         !key.startsWith("bundle.") && !key.startsWith("fab.") &&
                         !key.startsWith("override.") && !key.startsWith("attribute.")) {
-                agentProperties.add("  " + key + " = " + value);
+                profileProperties.add("  " + key + " = " + value);
             }
         }
 
@@ -131,8 +131,8 @@ public class ProfileDisplay implements Action {
                 printConfigList("Overrides : ", output, profile.getOverrides());
             }
 
-            if (agentProperties.size() > 0) {
-                printConfigList("Agent Properties : ", output, agentProperties);
+            if (profileProperties.size() > 0) {
+                printConfigList("Profile Properties : ", output, profileProperties);
             }
 
             if (systemProperties.size() > 0) {
diff --git a/profile/src/main/java/org/apache/karaf/profile/command/ProfileEdit.java b/profile/src/main/java/org/apache/karaf/profile/command/ProfileEdit.java
index 596731a..6e52086 100644
--- a/profile/src/main/java/org/apache/karaf/profile/command/ProfileEdit.java
+++ b/profile/src/main/java/org/apache/karaf/profile/command/ProfileEdit.java
@@ -26,6 +26,7 @@ import java.util.Map;
 
 import org.apache.karaf.profile.Profile;
 import org.apache.karaf.profile.ProfileBuilder;
+import org.apache.karaf.profile.ProfileConstants;
 import org.apache.karaf.profile.ProfileService;
 import org.apache.karaf.shell.api.action.Action;
 import org.apache.karaf.shell.api.action.Argument;
@@ -48,15 +49,6 @@ public class ProfileEdit implements Action {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(ProfileEdit.class);
 
-    static final String FEATURE_PREFIX = "feature.";
-    static final String REPOSITORY_PREFIX = "repository.";
-    static final String BUNDLE_PREFIX = "bundle.";
-    static final String OVERRIDE_PREFIX = "override.";
-    static final String CONFIG_PREFIX = "config.";
-    static final String SYSTEM_PREFIX = "system.";
-    static final String LIB_PREFIX = "lib.";
-    static final String ENDORSED_PREFIX = "endorsed.";
-    static final String EXT_PREFIX = "ext.";
     static final String DELIMITER = ",";
     static final String PID_KEY_SEPARATOR = "/";
 
@@ -156,15 +148,15 @@ public class ProfileEdit implements Action {
         }
         if (libs != null && libs.length > 0) {
             editInLine = true;
-            handleLibraries(builder, libs, profile, "lib", LIB_PREFIX);
+            handleLibraries(builder, libs, profile, "lib", ProfileConstants.LIB_PREFIX);
         }
         if (endorsed != null && endorsed.length > 0) {
             editInLine = true;
-            handleLibraries(builder, endorsed, profile, "endorsed lib", ENDORSED_PREFIX);
+            handleLibraries(builder, endorsed, profile, "endorsed lib", ProfileConstants.ENDORSED_PREFIX);
         }
         if (extension != null && extension.length > 0) {
             editInLine = true;
-            handleLibraries(builder, extension, profile, "extension lib", EXT_PREFIX);
+            handleLibraries(builder, extension, profile, "extension lib", ProfileConstants.EXT_PREFIX);
         }
         if (bundles != null && bundles.length > 0) {
             editInLine = true;
@@ -215,7 +207,7 @@ public class ProfileEdit implements Action {
             } else {
                 System.out.println("Adding feature:" + feature + " to profile:" + profile.getId());
             }
-            updateConfig(conf, FEATURE_PREFIX + feature.replace('/', '_'), feature, set, delete);
+            updateConfig(conf, ProfileConstants.FEATURE_PREFIX + feature.replace('/', '_'), feature, set, delete);
             builder.addConfiguration(Profile.INTERNAL_PID, conf);
         }
     }
@@ -231,7 +223,7 @@ public class ProfileEdit implements Action {
             } else if (delete) {
                 System.out.println("Deleting feature repository:" + repositoryURI + " from profile:" + profile.getId());
             }
-            updateConfig(conf, REPOSITORY_PREFIX + repositoryURI.replace('/', '_'), repositoryURI, set, delete);
+            updateConfig(conf, ProfileConstants.REPOSITORY_PREFIX + repositoryURI.replace('/', '_'), repositoryURI, set, delete);
         }
         builder.addConfiguration(Profile.INTERNAL_PID, conf);
     }
@@ -269,7 +261,7 @@ public class ProfileEdit implements Action {
             } else if (delete) {
                 System.out.println("Deleting bundle:" + bundle + " from profile:" + profile.getId());
             }
-            updateConfig(conf, BUNDLE_PREFIX + bundle.replace('/', '_'), bundle, set, delete);
+            updateConfig(conf, ProfileConstants.BUNDLE_PREFIX + bundle.replace('/', '_'), bundle, set, delete);
         }
         builder.addConfiguration(Profile.INTERNAL_PID, conf);
     }
@@ -287,7 +279,7 @@ public class ProfileEdit implements Action {
             } else if (delete) {
                 System.out.println("Deleting override:" + override + " from profile:" + profile.getId());
             }
-            updateConfig(conf, OVERRIDE_PREFIX + override.replace('/', '_'), override, set, delete);
+            updateConfig(conf, ProfileConstants.OVERRIDE_PREFIX + override.replace('/', '_'), override, set, delete);
         }
         builder.addConfiguration(Profile.INTERNAL_PID, conf);
     }
@@ -375,7 +367,7 @@ public class ProfileEdit implements Action {
                 } else {
                     System.out.println("Removing value:" + value + " key:" + key + " from system properties and profile:" + profile.getId());
                 }
-                updatedDelimitedList(conf, SYSTEM_PREFIX + key, value, delimiter, set, delete, append, remove);
+                updatedDelimitedList(conf, ProfileConstants.SYSTEM_PREFIX + key, value, delimiter, set, delete, append, remove);
             }
         }
         builder.addConfiguration(Profile.INTERNAL_PID, conf);
@@ -400,7 +392,7 @@ public class ProfileEdit implements Action {
                 } else if (set) {
                     System.out.println("Setting value:" + value + " key:" + key + " from config properties and profile:" + profile.getId());
                 }
-                updatedDelimitedList(conf, CONFIG_PREFIX + key, value, delimiter, set, delete, append, remove);
+                updatedDelimitedList(conf, ProfileConstants.CONFIG_PREFIX + key, value, delimiter, set, delete, append, remove);
             }
         }
         builder.addConfiguration(Profile.INTERNAL_PID, conf);
diff --git a/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java
index 532caf6..10331bd 100644
--- a/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java
+++ b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java
@@ -38,8 +38,6 @@ import static org.apache.karaf.profile.impl.ProfileImpl.ConfigListType;
  */
 public final class ProfileBuilderImpl implements ProfileBuilder {
 
-    private static final String PARENTS_ATTRIBUTE_KEY = Profile.ATTRIBUTE_PREFIX + Profile.PARENTS;
-
 	private String profileId;
 	private Map<String, byte[]> fileMapping = new HashMap<>();
 	private boolean isOverlay;
@@ -60,7 +58,7 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
 	@Override
     public List<String> getParents() {
         Map<String, Object> config = getConfigurationInternal(Profile.INTERNAL_PID);
-        String pspec = (String) config.get(PARENTS_ATTRIBUTE_KEY);
+        String pspec = (String) config.get(Profile.PARENTS);
         String[] parentIds = pspec != null ? pspec.split(" ") : new String[0];
         return Arrays.asList(parentIds);
     }
@@ -102,9 +100,9 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
 
     private void updateParentsAttribute(Collection<String> parentIds) {
         Map<String, Object> config = getConfigurationInternal(Profile.INTERNAL_PID);
-        config.remove(PARENTS_ATTRIBUTE_KEY);
+        config.remove(Profile.PARENTS);
         if (parentIds.size() > 0) {
-            config.put(PARENTS_ATTRIBUTE_KEY, parentsAttributeValue(parentIds));
+            config.put(Profile.PARENTS, parentsAttributeValue(parentIds));
         }
         addConfiguration(Profile.INTERNAL_PID, config);
     }
@@ -195,49 +193,49 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
     
 	@Override
 	public ProfileBuilder setBundles(List<String> values) {
-		addAgentConfiguration(ConfigListType.BUNDLES, values);
+		addProfileConfiguration(ConfigListType.BUNDLES, values);
 		return this;
 	}
 
     @Override
     public ProfileBuilder addBundle(String value) {
-        addAgentConfiguration(ConfigListType.BUNDLES, value);
+        addProfileConfiguration(ConfigListType.BUNDLES, value);
         return this;
     }
 
     @Override
 	public ProfileBuilder setFeatures(List<String> values) {
-		addAgentConfiguration(ConfigListType.FEATURES, values);
+		addProfileConfiguration(ConfigListType.FEATURES, values);
 		return this;
 	}
 
     @Override
     public ProfileBuilder addFeature(String value) {
-        addAgentConfiguration(ConfigListType.FEATURES, value);
+        addProfileConfiguration(ConfigListType.FEATURES, value);
         return this;
     }
 
     @Override
 	public ProfileBuilder setRepositories(List<String> values) {
-		addAgentConfiguration(ConfigListType.REPOSITORIES, values);
+		addProfileConfiguration(ConfigListType.REPOSITORIES, values);
 		return this;
 	}
 
     @Override
     public ProfileBuilder addRepository(String value) {
-        addAgentConfiguration(ConfigListType.REPOSITORIES, value);
+        addProfileConfiguration(ConfigListType.REPOSITORIES, value);
         return this;
     }
 
     @Override
 	public ProfileBuilder setOverrides(List<String> values) {
-		addAgentConfiguration(ConfigListType.OVERRIDES, values);
+		addProfileConfiguration(ConfigListType.OVERRIDES, values);
 		return this;
 	}
 
     @Override
     public ProfileBuilder setOptionals(List<String> values) {
-        addAgentConfiguration(ConfigListType.OPTIONALS, values);
+        addProfileConfiguration(ConfigListType.OPTIONALS, values);
         return this;
     }
 
@@ -267,7 +265,7 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
         return null;
     }
 
-    private void addAgentConfiguration(ConfigListType type, List<String> values) {
+    private void addProfileConfiguration(ConfigListType type, List<String> values) {
         String prefix = type + ".";
         Map<String, Object> config = getConfigurationInternal(Profile.INTERNAL_PID);
         for (String key : new ArrayList<>(config.keySet())) {
@@ -281,7 +279,7 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
         addConfiguration(Profile.INTERNAL_PID, config);
     }
 
-    private void addAgentConfiguration(ConfigListType type, String value) {
+    private void addProfileConfiguration(ConfigListType type, String value) {
         String prefix = type + ".";
         Map<String, Object> config = getConfigurationInternal(Profile.INTERNAL_PID);
         config.put(prefix + value, value);
diff --git a/profile/src/main/java/org/apache/karaf/profile/impl/ProfileImpl.java b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileImpl.java
index bcd1b03..c8707ea 100644
--- a/profile/src/main/java/org/apache/karaf/profile/impl/ProfileImpl.java
+++ b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileImpl.java
@@ -37,7 +37,7 @@ import static org.apache.karaf.profile.impl.Utils.assertTrue;
  */
 final class ProfileImpl implements Profile {
 
-    private static final Pattern ALLOWED_PROFILE_NAMES_PATTERN = Pattern.compile("^[A-Za-z0-9]+[\\.A-Za-z0-9_-]*$");
+    private static final Pattern ALLOWED_PROFILE_NAMES_PATTERN = Pattern.compile("^[A-Za-z0-9]+[.A-Za-z0-9_-]*$");
 
     private final String profileId;
     private final Map<String, String> attributes;
@@ -53,7 +53,7 @@ final class ProfileImpl implements Profile {
         assertNotNull(profileId, "profileId is null");
         assertNotNull(parents, "parents is null");
         assertNotNull(fileConfigs, "fileConfigs is null");
-        assertTrue(ALLOWED_PROFILE_NAMES_PATTERN.matcher(profileId).matches(), "Profile id '" + profileId + "' is invalid. Profile id must be: lower-case letters, numbers, and . _ or - characters");
+        assertTrue(ALLOWED_PROFILE_NAMES_PATTERN.matcher(profileId).matches(), "Profile id '" + profileId + "' is invalid. Profile id must be: upper-case or lower-case letters, numbers, and . _ or - characters");
 
         this.profileId = profileId;
         this.isOverlay = isOverlay;
@@ -72,14 +72,10 @@ final class ProfileImpl implements Profile {
             }
         }
 
-        // Attributes are agent configuration with prefix 'attribute.'
+        // Attributes are profile configuration properties with prefix "attribute." contained in "profile" PID
         attributes = getPrefixedMap(ATTRIBUTE_PREFIX);
     }
 
-    public String getId() {
-        return profileId;
-    }
-
     @Override
     public Map<String, String> getAttributes() {
         return Collections.unmodifiableMap(attributes);
@@ -95,12 +91,17 @@ final class ProfileImpl implements Profile {
         return getPrefixedMap(SYSTEM_PREFIX);
     }
 
+    @Override
+    public String getId() {
+        return profileId;
+    }
+
     private Map<String, String> getPrefixedMap(String prefix) {
         Map<String, String> map = new HashMap<>();
-        Map<String, Object> agentConfig = configurations.get(Profile.INTERNAL_PID);
-        if (agentConfig != null) {
+        Map<String, Object> profileConfig = configurations.get(Profile.INTERNAL_PID);
+        if (profileConfig != null) {
             int prefixLength = prefix.length();
-            for (Entry<String, Object> entry : agentConfig.entrySet()) {
+            for (Entry<String, Object> entry : profileConfig.entrySet()) {
                 String key = entry.getKey();
                 if (key.startsWith(prefix)) {
                     map.put(key.substring(prefixLength), entry.getValue().toString());
@@ -111,8 +112,8 @@ final class ProfileImpl implements Profile {
     }
 
     @Override
-    public List<String> getLibraries() {
-        return getContainerConfigList(ConfigListType.LIBRARIES);
+    public List<String> getParentIds() {
+        return Collections.unmodifiableList(parents);
     }
 
     @Override
@@ -131,6 +132,26 @@ final class ProfileImpl implements Profile {
     }
 
     @Override
+    public List<String> getLibraries() {
+        return getContainerConfigList(ConfigListType.LIBRARIES);
+    }
+
+    @Override
+    public List<String> getBootLibraries() {
+        return getContainerConfigList(ConfigListType.BOOT_LIBRARIES);
+    }
+
+    @Override
+    public List<String> getEndorsedLibraries() {
+        return getContainerConfigList(ConfigListType.ENDORSED_LIBRARIES);
+    }
+
+    @Override
+    public List<String> getExtLibraries() {
+        return getContainerConfigList(ConfigListType.EXT_LIBRARIES);
+    }
+
+    @Override
     public List<String> getOverrides() {
         return getContainerConfigList(ConfigListType.OVERRIDES);
     }
@@ -141,26 +162,27 @@ final class ProfileImpl implements Profile {
     }
 
     @Override
-    public List<String> getParentIds() {
-        return Collections.unmodifiableList(parents);
+    public boolean isOverlay() {
+        return isOverlay;
     }
 
     @Override
     public boolean isAbstract() {
-        return parseBoolean(getAttributes().get(ABSTRACT));
+        return parseBoolean(attributes.get(ABSTRACT));
     }
 
     @Override
     public boolean isHidden() {
-        return parseBoolean(getAttributes().get(HIDDEN));
+        return parseBoolean(attributes.get(HIDDEN));
     }
 
     private Boolean parseBoolean(Object obj) {
-        return obj instanceof Boolean ? (Boolean) obj : Boolean.parseBoolean(obj.toString());
+        return obj instanceof Boolean ? (Boolean) obj : obj != null && Boolean.parseBoolean(obj.toString());
     }
 
-    public boolean isOverlay() {
-        return isOverlay;
+    @Override
+    public Set<String> getConfigurationFileNames() {
+        return Collections.unmodifiableSet(fileConfigurations.keySet());
     }
 
     @Override
@@ -169,15 +191,11 @@ final class ProfileImpl implements Profile {
     }
 
     @Override
-    public Set<String> getConfigurationFileNames() {
-        return Collections.unmodifiableSet(fileConfigurations.keySet());
-    }
-
-    @Override
     public byte[] getFileConfiguration(String fileName) {
         return fileConfigurations.get(fileName);
     }
 
+    @Override
     public Map<String, Map<String, Object>> getConfigurations() {
         return Collections.unmodifiableMap(configurations);
     }
@@ -230,13 +248,16 @@ final class ProfileImpl implements Profile {
 
     @Override
     public String toString() {
-        return "Profile[id=" + profileId + ",attrs=" + getAttributes() + "]";
+        return "Profile[id=" + profileId + ", attrs=" + getAttributes() + "]";
     }
 
     enum ConfigListType {
         BUNDLES("bundle"),
         FEATURES("feature"),
         LIBRARIES("library"),
+        BOOT_LIBRARIES("boot"),
+        ENDORSED_LIBRARIES("endorsed"),
+        EXT_LIBRARIES("ext"),
         OPTIONALS("optional"),
         OVERRIDES("override"),
         REPOSITORIES("repository");
@@ -251,4 +272,5 @@ final class ProfileImpl implements Profile {
             return value;
         }
     }
+
 }
diff --git a/profile/src/main/java/org/apache/karaf/profile/impl/Profiles.java b/profile/src/main/java/org/apache/karaf/profile/impl/Profiles.java
index 9fbfdcc..c3843d6 100644
--- a/profile/src/main/java/org/apache/karaf/profile/impl/Profiles.java
+++ b/profile/src/main/java/org/apache/karaf/profile/impl/Profiles.java
@@ -38,16 +38,29 @@ import org.apache.karaf.profile.ProfileBuilder;
 
 import static org.apache.karaf.profile.impl.Utils.assertNotNull;
 
+/**
+ * Static utilities to work with {@link Profile profiles}.
+ */
 public final class Profiles {
 
     public static final String PROFILE_FOLDER_SUFFIX = ".profile";
 
+    /**
+     * <p>Loads profiles from given directory path. A profile is represented as directory with <code>.profile</code>
+     * extension. Subdirectories constitute part of {@linl Profile#getId} - directory separators are changed to
+     * <code>-</code>.</p>
+     * <p>For example, profile contained in directory <code>mq/broker/standalone.profile</code> will have
+     * id = <code>mq-broker-standalone</code>.</p>
+     * @param root
+     * @return
+     * @throws IOException
+     */
     public static Map<String, Profile> loadProfiles(final Path root) throws IOException {
         final Map<String, Profile> profiles = new HashMap<>();
         Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
                 ProfileBuilder builder;
                 @Override
-                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                     Path fileName = dir.getFileName();
                     if (fileName != null && (fileName.toString().endsWith(PROFILE_FOLDER_SUFFIX)
                             || fileName.toString().endsWith(PROFILE_FOLDER_SUFFIX + "/"))) {
@@ -87,6 +100,12 @@ public final class Profiles {
         return profiles;
     }
 
+    /**
+     * Deletes profile by given {@link Profile#getId()} from <code>root</code> path.
+     * @param root
+     * @param id
+     * @throws IOException
+     */
     public static void deleteProfile(Path root, String id) throws IOException {
         Path path = root.resolve(id.replaceAll("-", root.getFileSystem().getSeparator()) + PROFILE_FOLDER_SUFFIX);
         if (Files.isDirectory(path)) {
@@ -105,6 +124,13 @@ public final class Profiles {
         }
     }
 
+    /**
+     * Writes given {@link Profile} under a path specified as <code>root</code>. Directory name to store a profile is
+     * derived from {@link Profile#getId()}
+     * @param root
+     * @param profile
+     * @throws IOException
+     */
     public static void writeProfile(Path root, Profile profile) throws IOException {
         Path path = root.resolve(profile.getId().replaceAll("-", root.getFileSystem().getSeparator()) + PROFILE_FOLDER_SUFFIX);
         Files.createDirectories(path);
@@ -113,10 +139,28 @@ public final class Profiles {
         }
     }
 
+    /**
+     * <p>Gets an <em>overlay</em> profile for given <code>profile</code>, where passed in map of additional profiles
+     * is searched for possible parent profiles of given <code>profile</code>.</p>
+     * @param profile
+     * @param profiles
+     * @return
+     */
     public static Profile getOverlay(Profile profile, Map<String, Profile> profiles) {
         return getOverlay(profile, profiles, null);
     }
 
+    /**
+     * <p>Gets an <em>overlay</em> profile for given <code>profile</code>, where passed in map of additional profiles
+     * is searched for possible parent profiles of given <code>profile</code>.</p>
+     * <p><code>environment</code> may be used to select different <em>variants</em> of profile configuration files.
+     * For example, if <code>environment</code> is specified, configuration for <code>my.pid</code> PID will be read
+     * from <code>my.pid.cfg#&lt;environment&gt;</code>.</p>
+     * @param profile
+     * @param profiles
+     * @param environment
+     * @return
+     */
     public static Profile getOverlay(Profile profile, Map<String, Profile> profiles, String environment) {
         assertNotNull(profile, "profile is null");
         assertNotNull(profile, "profiles is null");
@@ -130,22 +174,49 @@ public final class Profiles {
         }
     }
 
+    /**
+     * Gets an <code>effective</code> profile with single property placeholder resolver for <code>${profile:xxx}</code>
+     * placeholders and with <code>finalSubstitution</code> set to <code>true</code>.
+     * @param profile
+     * @return
+     */
     public static Profile getEffective(final Profile profile) {
-        return getEffective(profile,
-                true);
+        return getEffective(profile, true);
     }
 
+    /**
+     * Gets an <code>effective</code> profile with single property placeholder resolver for <code>${profile:xxx}</code>
+     * placeholders.
+     * @param profile
+     * @param finalSubstitution
+     * @return
+     */
     public static Profile getEffective(final Profile profile, boolean finalSubstitution) {
         return getEffective(profile,
                 Collections.singleton(new PlaceholderResolvers.ProfilePlaceholderResolver()),
                 finalSubstitution);
     }
 
+    /**
+     * Gets an <code>effective</code> profile with <code>finalSubstitution</code> set to <code>true</code>.
+     * @param profile
+     * @param resolvers
+     * @return
+     */
     public static Profile getEffective(final Profile profile,
                                        final Collection<PlaceholderResolver> resolvers) {
         return getEffective(profile, resolvers, true);
     }
 
+    /**
+     * <p>Gets an <em>effective</em> profile for given <code>profile</code>. Effective profile has all property
+     * placeholders resolved. When <code>finalSubstitution</code> is <code>true</code>, placeholders that can't
+     * be resolved are replaced with empty strings. When it's <code>false</code>, placeholders are left unchanged.</p>
+     * @param profile
+     * @param resolvers
+     * @param finalSubstitution
+     * @return
+     */
     public static Profile getEffective(final Profile profile,
                                        final Collection<PlaceholderResolver> resolvers,
                                        boolean finalSubstitution) {
@@ -208,6 +279,28 @@ public final class Profiles {
         return builder.getProfile();
     }
 
+    /**
+     * <p>Helper internal class to configure {@link ProfileBuilder} used to create an <em>overlay</em> profile.</p>
+     * <p>There are strict rules built on a concept of profiles being <em>containers of file configurations</em>.
+     * Each profile may contain files with the same name. Profiles may be set in multi-parent - child relationship.
+     * Such graph of profiles is searched in depth-first fashion, while child (being a root of the graph) has
+     * highest priority.</p>
+     * <p>Files from higher-priority profile override files from parent profiles. Special case are PID files (with
+     * {@link Profile#PROPERTIES_SUFFIX} extension). These files are not simply taken from child profiles. Child
+     * profiles may have own version of given PID configuration file, but these files are overwritten at property
+     * level.</p>
+     * <p>For example, if parent profile specifies:<pre>
+     * property1 = v1
+     * property2 = v2
+     * </pre> and child profile specifies:<pre>
+     * property1 = v1a
+     * property3 = v3a
+     * </pre>an <em>overlay</em> profile for child profile uses:<pre>
+     * property1 = v1a
+     * property2 = v2
+     * property3 = v3a
+     * </pre></p>
+     */
     static private class OverlayOptionsProvider {
 
         private final Map<String, Profile> profiles;
diff --git a/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java b/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java
index 056332c..5a1e886 100644
--- a/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java
+++ b/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java
@@ -16,17 +16,65 @@
  */
 package org.apache.karaf.profile.impl;
 
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.UUID;
 
 import org.apache.karaf.profile.Profile;
 import org.apache.karaf.profile.ProfileBuilder;
 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.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 public class ProfilesTest {
 
+    public static Logger LOG = LoggerFactory.getLogger(ProfilesTest.class);
+
+    @Test
+    public void testProfilesApi() throws IOException {
+        ProfileBuilder builder = ProfileBuilder.Factory.create("my-simple-profile");
+        builder.addParents(Collections.emptyList());
+        builder.addAttribute("attr1", "val1");
+        builder.addBundle("mvn:commons-everything/commons-everything/42");
+        builder.addConfiguration("my.pid", "a1", "v1${profile:my.pid2/a2}");
+        builder.addConfiguration("my.pid", "a2", "v1${profile:my.pid2/a3}");
+        builder.addFeature("feature1");
+        builder.addFileConfiguration("my.pid2.txt", "hello!".getBytes("UTF-8"));
+        builder.addFileConfiguration("my.pid2.cfg", "a2=v2".getBytes("UTF-8"));
+        builder.addRepository("mvn:my/repository/1/xml/features");
+        builder.setOptionals(Arrays.asList("mvn:g/a/1", "mvn:g/a/2"));
+        builder.setOverrides(Arrays.asList("mvn:g/a/4", "mvn:g/a/3"));
+        Profile profile = builder.getProfile();
+        LOG.info("Profile: {}", profile.toString());
+        LOG.info("Config: {}", profile.getConfig());
+        LOG.info("Libraries: {}", profile.getLibraries());
+        LOG.info("System: {}", profile.getSystem());
+        LOG.info("Configurations: {}", profile.getConfigurations());
+        LOG.info("ConfigurationFileNames: {}", profile.getConfigurationFileNames());
+        LOG.info("FileConfigurations: {}", profile.getFileConfigurations().keySet());
+
+        Profile effectiveProfile1 = Profiles.getEffective(profile, false);
+        Profile effectiveProfile2 = Profiles.getEffective(profile, true);
+        Map<String, Profile> profiles = new HashMap<>();
+        profiles.put("x", profile);
+        Profile overlayProfile = Profiles.getOverlay(profile, profiles);
+        Profiles.writeProfile(Paths.get("target/p-" + UUID.randomUUID().toString()), profile);
+        Profiles.writeProfile(Paths.get("target/ep1-" + UUID.randomUUID().toString()), effectiveProfile1);
+        Profiles.writeProfile(Paths.get("target/ep2-" + UUID.randomUUID().toString()), effectiveProfile2);
+        Profiles.writeProfile(Paths.get("target/op-" + UUID.randomUUID().toString()), overlayProfile);
+    }
+
     @Test
     public void testProfilePlaceholderResolver() {
         Profile profile = ProfileBuilder.Factory.create("test")
@@ -87,4 +135,98 @@ public class ProfilesTest {
         String outPid1 = new String(overlay.getFileConfiguration("pid1.cfg"));
         assertEquals(String.format("%1$s%n%2$s%n","# My comment","foo = bar2"), outPid1);
     }
+
+    @Test
+    public void overlayProfiles() {
+        Profile p1 = ProfileBuilder.Factory.create("p1")
+                .addAttribute("p1a1", "p1v1")
+                .addConfiguration("p1p1", "p1p1p1", "p1p1v1")
+                .addConfiguration("pp1", "pp1p1", "p1p1v1")
+                .getProfile();
+        Profile p2 = ProfileBuilder.Factory.create("p2")
+                .addAttribute("p2a1", "p2v1")
+                .addConfiguration("p2p1", "p2p1p1", "p2p1v1")
+                .addConfiguration("pp1", "pp1p1", "p2p1v1")
+                .getProfile();
+
+        Profile c1 = ProfileBuilder.Factory.create("c2")
+                .addParents(Arrays.asList("p1", "p2"))
+                .getProfile();
+
+        assertThat(c1.getAttributes().get("p1a1"), nullValue());
+        assertThat(c1.getAttributes().get("p2a1"), nullValue());
+        assertThat(c1.getConfigurations().size(), equalTo(1));
+        assertTrue(c1.getConfigurations().containsKey("profile"));
+
+        Map<String, Profile> parents = new LinkedHashMap<>();
+        parents.put("p1", p1);
+        parents.put("p2", p2);
+        Profile oc1 = Profiles.getOverlay(c1, parents);
+        assertThat(oc1.getAttributes().get("p1a1"), equalTo("p1v1"));
+        assertThat(oc1.getAttributes().get("p2a1"), equalTo("p2v1"));
+        assertThat(oc1.getConfigurations().size(), equalTo(4));
+        assertTrue(oc1.getConfigurations().containsKey("p1p1"));
+        assertTrue(oc1.getConfigurations().containsKey("p2p1"));
+        assertTrue(oc1.getConfigurations().containsKey("pp1"));
+        assertTrue(oc1.getConfigurations().containsKey("profile"));
+    }
+
+    @Test
+    public void inheritanceOrder() {
+        Profile gp1 = ProfileBuilder.Factory.create("gp1")
+                .addAttribute("a", "1")
+                .addFileConfiguration("f", new byte[] { 0x01 })
+                .addAttribute("b", "1")
+                .addAttribute("c", "1")
+                .addConfiguration("p", "p", "1")
+                .addConfiguration("p", "px", "1")
+                .getProfile();
+        Profile gp2 = ProfileBuilder.Factory.create("gp2")
+                .addAttribute("a", "2")
+                .addAttribute("c", "2")
+                .addFileConfiguration("f", new byte[] { 0x02 })
+                .addConfiguration("p", "p", "2")
+                .getProfile();
+        Profile p1 = ProfileBuilder.Factory.create("p1")
+                .addParents(Arrays.asList("gp1", "gp2"))
+                .addAttribute("a", "3")
+                .addFileConfiguration("f", new byte[] { 0x03 })
+                .addConfiguration("p", "p", "3")
+                .getProfile();
+        Profile p2 = ProfileBuilder.Factory.create("p2")
+                .addAttribute("a", "4")
+                .addAttribute("b", "4")
+                .addFileConfiguration("f", new byte[] { 0x04 })
+                .addConfiguration("p", "p", "4")
+                .getProfile();
+        Profile c = ProfileBuilder.Factory.create("p2")
+                .addParents(Arrays.asList("p1", "p2"))
+                .addAttribute("a", "5")
+                .addFileConfiguration("f", new byte[] { 0x05 })
+                .addConfiguration("p", "p", "5")
+                .getProfile();
+
+        Map<String, Profile> parents = new LinkedHashMap<>();
+        parents.put("gp1", gp1);
+        parents.put("gp2", gp2);
+        parents.put("p1", p1);
+        parents.put("p2", p2);
+
+        assertThat(Profiles.getOverlay(c, parents).getAttributes().get("a"), equalTo("5"));
+        assertThat(Profiles.getOverlay(c, parents).getAttributes().get("b"), equalTo("4"));
+        assertThat(Profiles.getOverlay(c, parents).getAttributes().get("c"), equalTo("2"));
+        assertThat(Profiles.getOverlay(c, parents).getConfiguration("p").get("p"), equalTo("5"));
+        assertThat(Profiles.getOverlay(c, parents).getConfiguration("p").get("px"), equalTo("1"));
+        assertThat(Profiles.getOverlay(c, parents).getFileConfiguration("f"), equalTo(new byte[] { 0x05 }));
+    }
+
+    @Test
+    public void overrides() {
+        Profile p = ProfileBuilder.Factory.create("p")
+                .setOverrides(Arrays.asList("a", "b"))
+                .getProfile();
+
+        assertThat(p.getConfiguration("profile").size(), equalTo(2));
+    }
+
 }
diff --git a/tooling/karaf-maven-plugin/pom.xml b/tooling/karaf-maven-plugin/pom.xml
index c957c63..67feb65 100644
--- a/tooling/karaf-maven-plugin/pom.xml
+++ b/tooling/karaf-maven-plugin/pom.xml
@@ -226,10 +226,6 @@
             <resource>
                 <directory>${project.basedir}/src/main/resources</directory>
             </resource>
-            <resource>
-                <directory>${project.basedir}/src/main/filtered-resources</directory>
-                <filtering>true</filtering>
-            </resource>
         </resources>
         <testResources>
             <testResource>
diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java
index 6671614..f8d78b9 100644
--- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java
+++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java
@@ -20,6 +20,7 @@ package org.apache.karaf.tooling;
 
 import javax.xml.namespace.QName;
 import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamReader;
 import java.io.File;
 import java.io.FileInputStream;
@@ -30,10 +31,11 @@ import java.nio.file.attribute.PosixFilePermissions;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Properties;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 import java.util.stream.Collectors;
@@ -46,6 +48,7 @@ import org.apache.karaf.tooling.utils.MojoSupport;
 import org.apache.karaf.tooling.utils.ReactorMavenResolver;
 import org.apache.karaf.tools.utils.model.KarafPropertyEdits;
 import org.apache.karaf.tools.utils.model.io.stax.KarafPropertyInstructionsModelStaxReader;
+import org.apache.karaf.util.Version;
 import org.apache.maven.artifact.Artifact;
 import org.apache.maven.plugin.MojoExecutionException;
 import org.apache.maven.plugin.MojoFailureException;
@@ -54,17 +57,21 @@ import org.apache.maven.plugins.annotations.LifecyclePhase;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.MavenProject;
 import org.eclipse.aether.repository.WorkspaceReader;
 import org.osgi.framework.Constants;
+import org.osgi.framework.launch.FrameworkFactory;
 
 /**
- * Creates a customized Karaf distribution by installing features and setting up
- * configuration files. The plugin gets features from feature.xml files and KAR
+ * <p>Creates a customized Karaf distribution by installing features and setting up
+ * configuration files.</p>
+ *
+ * <p>The plugin gets features from feature.xml files and KAR
  * archives declared as dependencies or as files configured with the
- * featureRespositories parameter. It picks up other files, such as config files,
+ * [startup|boot|installed]Respositories parameters. It picks up other files, such as config files,
  * from ${project.build.directory}/classes. Thus, a file in src/main/resources/etc
  * will be copied by the resource plugin to ${project.build.directory}/classes/etc,
- * and then added to the assembly by this goal.
+ * and then added to the assembly by this goal.</p>
  */
 @Mojo(name = "assembly", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.RUNTIME, threadSafe = true)
 public class AssemblyMojo extends MojoSupport {
@@ -81,130 +88,260 @@ public class AssemblyMojo extends MojoSupport {
     @Parameter(defaultValue = "${project.build.directory}/assembly")
     protected File workDirectory;
 
-    /**
-     * Features configuration file (etc/org.apache.karaf.features.cfg).
+    /*
+     * There are three builder stages related to maven dependency scopes:
+     *  - Stage.Startup : scope=compile
+     *  - Stage.Boot : scope=runtime
+     *  - Stage.Installed : scope=provided
+     * There's special category not related to stage - Blacklisted
+     *
+     * There are five kinds of artifacts/dependencies that may go into any of the above stages/categories/scopes:
+     *  - kars: maven artifacts with "kar" type
+     *  - repositories: maven artifacts with "features" classifier
+     *  - features: Karaf feature names (name[/version])
+     *  - bundles: maven artifacts with "jar" or "bundle" type
+     *  - profiles: directories with Karaf 4 profiles
+     * (Not all artifacts/dependencies may be connected with every stage/category/scope.)
+     *
+     * Blacklisting:
+     *  - kars: there are no blacklisted kars
+     *  - repositories: won't be processed at all (also affects transitive repositories)
+     *  - features: will be removed from JAXB model of features XML after loading
+     *  - bundles: will be removed from features of JAXB model after loading
+     *  - profiles: will be removed
+     *
+     * Stage.Startup:
+     *  - bundles: will be put to etc/startup.properties
+     *  - features: their bundles will be put to etc/startup.properties
+     *  - repositories: will be used to resolve startup bundles/feature before adding them to etc/startup.properties
+     *  - kars: unpacked to assembly, detected features XML repositories added as Stage.Startup repositories
+     *
+     * Stage.Boot:
+     *  - bundles: special etc/<UUID>.xml features XML file will be created with <UUID> feature.
+     *      etc/org.apacha.karaf.features.cfg will have this features XML file in featuresRepositories property and
+     *      the feature itself in featuresBoot property
+     *  - features: will be added to etc/org.apacha.karaf.features.cfg file, featuresBoot property
+     *      also features from Stage.Startup will be used here.
+     *  - repositories: will be added to etc/org.apacha.karaf.features.cfg file, featuresRepositories property
+     *      also repositories from Stage.Startup will be used here.
+     *  - kars: unpacked to assembly, detected features XML repositories added as Stage.Boot repositories
+     *
+     * Stage.Installed:
+     *  - bundles: will be copied to system/
+     *  - features: their bundles and config files will be copied to system/
+     *  - repositories: will be used to find Stage.Installed features
+     *      also repositories from Stage.Boot will be searched for Stage.Installed features
+     *  - kars: unpacked to assembly, detected features XML repositories added as Stage.Installed repositories
      */
-    @Parameter(defaultValue = "${project.build.directory}/assembly/etc/org.apache.karaf.features.cfg")
-    protected File featuresCfgFile;
 
     /**
-     * startup.properties file.
+     * For given stage (startup, boot, install) if there are no stage-specific features and profiles, all features
+     * from stage-specific repositories will be used.
      */
-    @Parameter(defaultValue = "${project.build.directory}/assembly/etc/startup.properties")
-    protected File startupPropertiesFile;
+    @Parameter(defaultValue = "true")
+    protected boolean installAllFeaturesByDefault = true;
 
     /**
-     * Directory used during build to construction the Karaf system repository.
+     * An environment identifier that may be used to select different variant of PID configuration file, e.g.,
+     * <code>org.ops4j.pax.url.mvn.cfg#docker</code>.
      */
-    @Parameter(defaultValue="${project.build.directory}/assembly/system")
-    protected File systemDirectory;
+    @Parameter
+    private String environment;
 
     /**
-     * default start level for bundles in features that don't specify it.
+     * List of compile-scope features XML files to be used in startup stage (etc/startup.properties)
      */
     @Parameter
-    protected int defaultStartLevel = 30;
-
-    @Parameter
     private List<String> startupRepositories;
+    /**
+     * List of runtime-scope features XML files to be used in boot stage (etc/org.apache.karaf.features.cfg)
+     */
     @Parameter
     private List<String> bootRepositories;
+    /**
+     * List of provided-scope features XML files to be used in install stage
+     */
     @Parameter
     private List<String> installedRepositories;
-
+    /**
+     * List of blacklisted repository URIs. Blacklisted URI may use globs and version ranges. See
+     * {@link org.apache.karaf.features.LocationPattern}.
+     */
     @Parameter
     private List<String> blacklistedRepositories;
 
     /**
-     * List of features from runtime-scope features xml and kars to be installed into system and listed in startup.properties.
+     * List of features from compile-scope features XML files and KARs to be installed into system repo
+     * and listed in etc/startup.properties.
      */
     @Parameter
     private List<String> startupFeatures;
-
     /**
-     * List of features from runtime-scope features xml and kars to be installed into system repo and listed in features service boot features.
+     * List of features from runtime-scope features XML files and KARs to be installed into system repo
+     * and listed in featuresBoot property in etc/org.apache.karaf.features.cfg
      */
     @Parameter
     private List<String> bootFeatures;
-
     /**
-     * List of features from runtime-scope features xml and kars to be installed into system repo and not mentioned elsewhere.
+     * List of features from provided-scope features XML files and KARs to be installed into system repo
+     * and not mentioned elsewhere.
      */
     @Parameter
     private List<String> installedFeatures;
-
+    /**
+     * <p>List of feature blacklisting clauses. Each clause is in one of the formats ({@link org.apache.karaf.features.FeaturePattern}):<ul>
+     *     <li><code>feature-name</code></li>
+     *     <li><code>feature-name;range=version-or-range</code></li>
+     *     <li><code>feature-name/version-or-range</code></li>
+     * </ul></p>
+     */
     @Parameter
     private List<String> blacklistedFeatures;
 
+    /**
+     * List of compile-scope bundles added to etc/startup.properties
+     */
     @Parameter
     private List<String> startupBundles;
+    /**
+     * List of runtime-scope bundles wrapped in special feature added to featuresBoot property
+     * in etc/org.apache.karaf.features.cfg
+     */
     @Parameter
     private List<String> bootBundles;
+    /**
+     * List of provided-scope bundles added to system repo
+     */
     @Parameter
     private List<String> installedBundles;
+    /**
+     * List of blacklisted bundle URIs. Blacklisted URI may use globs and version ranges. See
+     * {@link org.apache.karaf.features.LocationPattern}.
+     */
     @Parameter
     private List<String> blacklistedBundles;
-    
-    @Parameter
-    private String profilesUri;
 
+    /**
+     * List of profile URIs to use
+     */
     @Parameter
-    private List<String> bootProfiles;
+    private List<String> profilesUris;
 
+    /**
+     * List of profiles names to load from configured <code>profilesUris</code> and use as startup profiles.
+     */
     @Parameter
     private List<String> startupProfiles;
-
+    /**
+     * List of profiles names to load from configured <code>profilesUris</code> and use as boot profiles.
+     */
+    @Parameter
+    private List<String> bootProfiles;
+    /**
+     * List of profiles names to load from configured <code>profilesUris</code> and use as installed profiles.
+     */
     @Parameter
     private List<String> installedProfiles;
-
+    /**
+     * List of blacklisted profile names (possibly using <code>*</code> glob)
+     */
     @Parameter
     private List<String> blacklistedProfiles;
 
+    /**
+     * When assembly custom distribution, we can include generated and added profiles in the distribution itself,
+     * in <code>${karaf.etc}/profiles</code> directory.
+     */
+    @Parameter(defaultValue = "false")
+    private boolean writeProfiles;
+
+    /*
+     * KARs are not configured using Maven plugin configuration, but rather detected from dependencies.
+     * All KARs are just unzipped into the assembly being constructed, but additionally KAR's embedded
+     * features XML repositories are added to relevant stage.
+     */
+
+    private List<String> startupKars = new ArrayList<>();
+    private List<String> bootKars = new ArrayList<>();
+    private List<String> installedKars = new ArrayList<>();
+
+    /**
+     * TODOCUMENT
+     */
     @Parameter
     private Builder.BlacklistPolicy blacklistPolicy = Builder.BlacklistPolicy.Discard;
 
     /**
-     * Ignore the dependency attribute (dependency="[true|false]") on bundle
+     * Ignore the dependency attribute (dependency="[true|false]") on bundles, effectively forcing their
+     * installation.
      */
     @Parameter(defaultValue = "false")
     protected boolean ignoreDependencyFlag;
 
     /**
-     * Additional feature repositories
+     * <p>Additional libraries to add into assembled distribution. Libraries are specified using
+     * <code>name[;url:=&lt;url&gt;][;type:=&lt;type&gt;][;export:=true|false][;delegate:=true|false]</code>
+     * syntax. If there's no <code>url</code> header directive, <code>name</code> is used as URI. Otherwise
+     * <code>name</code> is used as target file name to use.</p>
+     *
+     * <p><code>type</code> may be:<ul>
+     *     <li>endorsed - library will be added to <code>${karaf.home}/lib/endorsed</code></li>
+     *     <li>extension - library will be added to <code>${karaf.home}/lib/ext</code></li>
+     *     <li>boot - library will be added to <code>${karaf.home}/lib/boot</code></li>
+     *     <li>by default, library is put directly into <code>${karaf.home}/lib</code> - these libraries will
+     *     be used in default classloader for OSGi framework which will load {@link FrameworkFactory} implementation.</li>
+     * </ul></p>
+     *
+     * <p><code>export</code> flag determines whether packages from <code>Export-Package</code> manifest
+     * header of the library will be added to <code>org.osgi.framework.system.packages.extra</code> property in
+     * <code>${karaf.etc}/config.properties</code>.</p>
+     *
+     * <p><code>delegate</code> flag determines whether packages from <code>Export-Pavkage</code> manifest
+     * header of the library will be added to <code>org.osgi.framework.bootdelegation</code> property in
+     * <code>${karaf.etc}/config.properties</code>.</p>
      */
     @Parameter
-    protected List<String> featureRepositories;
-
-    @Parameter
     protected List<String> libraries;
 
     /**
-     * Use reference: style urls in startup.properties
+     * Use <code>reference:file:gr/oup/Id/artifactId/version/artifactId-version-classifier.type</code> style
+     * urls in <code>etc/startup.properties</code>.
      */
+    // see:
+    //  - org.apache.felix.framework.cache.BundleArchive.createRevisionFromLocation()
+    //  - org.apache.karaf.main.Main.installAndStartBundles()
     @Parameter(defaultValue = "false")
     protected boolean useReferenceUrls;
 
     /**
-     * Include project build output directory in the assembly
+     * Include project build output directory in the assembly. This allows (filtered or unfiltered) Maven
+     * resources directories to be used to provide additional resources in the assembly.
      */
     @Parameter(defaultValue = "true")
     protected boolean includeBuildOutputDirectory;
 
-    @Parameter
-    protected boolean installAllFeaturesByDefault = true;
-
+    /**
+     * Karaf version changes the way some configuration files are prepared (to adjust to given Karaf version
+     * requirements).
+     */
     @Parameter
     protected Builder.KarafVersion karafVersion = Builder.KarafVersion.v4x;
 
     /**
-     * Specify the version of Java SE to be assumed for osgi.ee.
+     * <p>Specify the version of Java SE to be assumed for osgi.ee. The value will be used in
+     * <code>etc/config.properties</code> file, in <code>java.specification.version</code> placeholder used in
+     * several properties:<ul>
+     *     <li><code>org.osgi.framework.system.packages</code></li>
+     *     <li><code>org.osgi.framework.system.capabilities</code></li>
+     * </ul></p>
+     * <p>Valid values are: 1.6, 1.7, 1.8, 9</p>
      */
     @Parameter(defaultValue = "1.8")
     protected String javase;
 
     /**
      * Specify which framework to use
-     * (one of framework, framework-logback, static-framework, static-framework-logback).
+     * (one of framework, framework-logback, static-framework, static-framework-logback, custom).
      */
     @Parameter
     protected String framework;
@@ -248,8 +385,8 @@ public class AssemblyMojo extends MojoSupport {
     protected String propertyFileEdits;
 
     /**
-     * Glob specifying which configuration pids in the selected boot features
-     * should be extracted to the etc directory.
+     * Glob specifying which configuration PIDs in the selected boot features
+     * should be extracted to <code>${karaf.etc}</code> directory. By default all PIDs are extracted.
      */
     @Parameter
     protected List<String> pidsToExtract = Collections.singletonList("*");
@@ -262,9 +399,15 @@ public class AssemblyMojo extends MojoSupport {
     @Parameter
     protected Map<String, String> translatedUrls;
 
+    /**
+     * Specify a list of additional properties that should be added to <code>${karaf.etc}/config.properties</code>
+     */
     @Parameter
     protected Map<String, String> config;
 
+    /**
+     * Specify a list of additional properties that should be added to <code>${karaf.etc}/system.properties</code>
+     */
     @Parameter
     protected Map<String, String> system;
 
@@ -274,77 +417,56 @@ public class AssemblyMojo extends MojoSupport {
     @Override
     public void execute() throws MojoExecutionException, MojoFailureException {
         try {
+            setNullListsToEmpty();
+            setNullMapsToEmpty();
+
             doExecute();
-        }
-        catch (MojoExecutionException | MojoFailureException e) {
+        } catch (MojoExecutionException | MojoFailureException e) {
             throw e;
-        }
-        catch (Exception e) {
+        } catch (Exception e) {
             throw new MojoExecutionException("Unable to build assembly", e);
         }
     }
 
+    /**
+     * Main processing method. Most of the work involves configuring and invoking {@link Builder a profile builder}.
+     * @throws Exception
+     */
     protected void doExecute() throws Exception {
-        startupRepositories = nonNullList(startupRepositories);
-        bootRepositories = nonNullList(bootRepositories);
-        installedRepositories = nonNullList(installedRepositories);
-        startupBundles = nonNullList(startupBundles);
-        bootBundles = nonNullList(bootBundles);
-        installedBundles = nonNullList(installedBundles);
-        blacklistedBundles = nonNullList(blacklistedBundles);
-        startupFeatures = nonNullList(startupFeatures);
-        bootFeatures = nonNullList(bootFeatures);
-        installedFeatures = nonNullList(installedFeatures);
-        blacklistedFeatures = nonNullList(blacklistedFeatures);
-        startupProfiles = nonNullList(startupProfiles);
-        bootProfiles = nonNullList(bootProfiles);
-        installedProfiles = nonNullList(installedProfiles);
-        blacklistedProfiles = nonNullList(blacklistedProfiles);
-        blacklistedRepositories = nonNullList(blacklistedRepositories);
-
         if (!startupProfiles.isEmpty() || !bootProfiles.isEmpty() || !installedProfiles.isEmpty()) {
-            if (profilesUri == null) {
-                throw new IllegalArgumentException("profilesDirectory must be specified");
-            }
-        }
-
-        if (featureRepositories != null && !featureRepositories.isEmpty()) {
-            getLog().warn("Use of featureRepositories is deprecated, use startupRepositories, bootRepositories or installedRepositories instead");
-            startupRepositories.addAll(featureRepositories);
-            bootRepositories.addAll(featureRepositories);
-            installedRepositories.addAll(featureRepositories);
-        }
-
-        StringBuilder remote = new StringBuilder();
-        for (Object obj : project.getRemoteProjectRepositories()) {
-            if (remote.length() > 0) {
-                remote.append(",");
-            }
-            remote.append(invoke(obj, "getUrl"));
-            remote.append("@id=").append(invoke(obj, "getId"));
-            if (!((Boolean) invoke(getPolicy(obj, false), "isEnabled"))) {
-                remote.append("@noreleases");
-            }
-            if ((Boolean) invoke(getPolicy(obj, true), "isEnabled")) {
-                remote.append("@snapshots");
+            if (profilesUris.size() == 0) {
+                throw new IllegalArgumentException("profilesUris option must be specified");
             }
         }
-        getLog().info("Using repositories: " + remote.toString());
 
         Builder builder = Builder.newInstance();
+
+        // Set up miscellaneous options
         builder.offline(mavenSession.isOffline());
         builder.localRepository(localRepo.getBasedir());
-        builder.mavenRepositories(remote.toString());
         builder.resolverWrapper((resolver) -> new ReactorMavenResolver(reactor, resolver));
         builder.javase(javase);
-
-        // Set up config and system props
-        if (config != null) {
-            config.forEach(builder::config);
-        }
-        if (system != null) {
-            system.forEach(builder::system);
+        builder.karafVersion(karafVersion);
+        builder.useReferenceUrls(useReferenceUrls);
+        builder.defaultAddAll(installAllFeaturesByDefault);
+        builder.ignoreDependencyFlag(ignoreDependencyFlag);
+        builder.propertyEdits(configurePropertyEdits());
+        builder.translatedUrls(configureTranslatedUrls());
+        builder.pidsToExtract(pidsToExtract);
+        builder.writeProfiles(writeProfiles);
+        builder.environment(environment);
+
+        // Set up remote repositories from Maven build, to be used by pax-url-aether resolver
+        String remoteRepositories = MavenUtil.remoteRepositoryList(project.getRemoteProjectRepositories());
+        getLog().info("Using repositories:");
+        for (String r : remoteRepositories.split(",")) {
+            getLog().info("   " + r);
         }
+        builder.mavenRepositories(remoteRepositories);
+
+        // Set up config and system properties
+        config.forEach(builder::config);
+        system.forEach(builder::system);
 
         // Set up blacklisted items
         builder.blacklistBundles(blacklistedBundles);
@@ -353,163 +475,37 @@ public class AssemblyMojo extends MojoSupport {
         builder.blacklistRepositories(blacklistedRepositories);
         builder.blacklistPolicy(blacklistPolicy);
 
-        if (propertyFileEdits != null) {
-            File file = new File(propertyFileEdits);
-            if (file.exists()) {
-                KarafPropertyEdits edits;
-                try (InputStream editsStream = new FileInputStream(propertyFileEdits)) {
-                    KarafPropertyInstructionsModelStaxReader kipmsr = new KarafPropertyInstructionsModelStaxReader();
-                    edits = kipmsr.read(editsStream, true);
-                }
-                builder.propertyEdits(edits);
-            }
-        }
-        builder.pidsToExtract(pidsToExtract);
-
-        Map<String, String> urls = new HashMap<>();
-        List<Artifact> artifacts = new ArrayList<>(project.getAttachedArtifacts());
-        artifacts.add(project.getArtifact());
-        for (Artifact artifact : artifacts) {
-            if (artifact.getFile() != null && artifact.getFile().exists()) {
-                String mvnUrl = "mvn:" + artifact.getGroupId() + "/" + artifact.getArtifactId()
-                        + "/" + artifact.getVersion();
-                String type = artifact.getType();
-                if ("bundle".equals(type)) {
-                    type = "jar";
-                }
-                if (!"jar".equals(type) || artifact.getClassifier() != null) {
-                    mvnUrl += "/" + type;
-                    if (artifact.getClassifier() != null) {
-                        mvnUrl += "/" + artifact.getClassifier();
-                    }
-                }
-                urls.put(mvnUrl, artifact.getFile().toURI().toString());
-            }
-        }
-        if (translatedUrls != null) {
-            urls.putAll(translatedUrls);
-        }
-        builder.translatedUrls(urls);
-
-        // creating system directory
-        getLog().info("Creating work directory");
+        // Creating system directory
+        configureWorkDirectory();
+        getLog().info("Creating work directory: " + workDirectory);
         builder.homeDirectory(workDirectory.toPath());
-        IoUtils.deleteRecursive(workDirectory);
-        workDirectory.mkdirs();
 
-        List<String> startupKars = new ArrayList<>();
-        List<String> bootKars = new ArrayList<>();
-        List<String> installedKars = new ArrayList<>();
+        // Loading KARs and features repositories
+        getLog().info("Loading direct KAR and features XML dependencies");
+        processDirectMavenDependencies();
 
-        // Loading kars and features repositories
-        getLog().info("Loading kar and features repositories dependencies");
-        for (Artifact artifact : project.getDependencyArtifacts()) {
-            Builder.Stage stage;
-            switch (artifact.getScope()) {
-            case "compile":
-                stage = Builder.Stage.Startup;
-                break;
-            case "runtime":
-                stage = Builder.Stage.Boot;
-                break;
-            case "provided":
-                stage = Builder.Stage.Installed;
-                break;
-            default:
-                continue;
-            }
-            String uri = artifactToMvn(artifact);
-            String type = getType(artifact);
-            if ("kar".equals(type)) {
-                switch (stage) {
-                case Startup:   startupKars.add(uri); break;
-                case Boot:      bootKars.add(uri); break;
-                case Installed: installedKars.add(uri); break;
-                }
-            } else if ("features".equals(type)) {
-                switch (stage) {
-                case Startup:   startupRepositories.add(uri); break;
-                case Boot:      bootRepositories.add(uri); break;
-                case Installed: installedRepositories.add(uri); break;
-                }
-            } else if ("bundle".equals(type)) {
-                switch (stage) {
-                case Startup:   startupBundles.add(uri); break;
-                case Boot:      bootBundles.add(uri); break;
-                case Installed: installedBundles.add(uri); break;
-                }
-            }
-        }
+        // Set up profiles and libraries
+        profilesUris.forEach(builder::profilesUris);
+        libraries.forEach(builder::libraries);
 
-        builder.karafVersion(karafVersion)
-               .useReferenceUrls(useReferenceUrls)
-               .defaultAddAll(installAllFeaturesByDefault)
-               .ignoreDependencyFlag(ignoreDependencyFlag);
-        if (profilesUri != null) {
-            builder.profilesUris(profilesUri);
-        }
-        if (libraries != null) {
-            builder.libraries(libraries.toArray(new String[libraries.size()]));
-        }
-        // Startup
-        boolean hasFrameworkKar = false;
-        for (String kar : startupKars) {
-            if (kar.startsWith("mvn:org.apache.karaf.features/framework/")
-                    || kar.startsWith("mvn:org.apache.karaf.features/static/")) {
-                hasFrameworkKar = true;
-                startupKars.remove(kar);
-                if (framework == null) {
-                    framework = kar.startsWith("mvn:org.apache.karaf.features/framework/")
-                            ? "framework" : "static-framework";
-                }
-                builder.kars(Builder.Stage.Startup, false, kar);
-                break;
-            }
-        }
-        if (!hasFrameworkKar) {
-            Properties versions = new Properties();
-            try (InputStream is = getClass().getResourceAsStream("versions.properties")) {
-                versions.load(is);
-            } catch (IOException e) {
-                throw new IllegalStateException(e);
-            }
-            String realKarafVersion = versions.getProperty("karaf-version");
-            String kar;
-            switch (framework) {
-                case "framework":
-                    kar = "mvn:org.apache.karaf.features/framework/" + realKarafVersion + "/xml/features";
-                    break;
-                case "framework-logback":
-                    kar = "mvn:org.apache.karaf.features/framework/" + realKarafVersion + "/xml/features";
-                    break;
-                case "static-framework":
-                    kar = "mvn:org.apache.karaf.features/static/" + realKarafVersion + "/xml/features";
-                    break;
-                case "static-framework-logback":
-                    kar = "mvn:org.apache.karaf.features/static/" + realKarafVersion + "/xml/features";
-                    break;
-                default:
-                    throw new IllegalArgumentException("Unsupported framework: " + framework);
-            }
-            builder.kars(Builder.Stage.Startup, false, kar);
-        }
-        if (!startupFeatures.contains(framework)) {
-            builder.features(Builder.Stage.Startup, framework);
-        }
+        // Startup stage
+        detectStartupKarsAndFeatures(builder);
         builder.defaultStage(Builder.Stage.Startup)
                .kars(toArray(startupKars))
                .repositories(startupFeatures.isEmpty() && startupProfiles.isEmpty() && installAllFeaturesByDefault, toArray(startupRepositories))
                .features(toArray(startupFeatures))
                .bundles(toArray(startupBundles))
                .profiles(toArray(startupProfiles));
-        // Boot
+
+        // Boot stage
         builder.defaultStage(Builder.Stage.Boot)
                 .kars(toArray(bootKars))
                 .repositories(bootFeatures.isEmpty() && bootProfiles.isEmpty() && installAllFeaturesByDefault, toArray(bootRepositories))
                 .features(toArray(bootFeatures))
                 .bundles(toArray(bootBundles))
                 .profiles(toArray(bootProfiles));
-        // Installed
+
+        // Installed stage
         builder.defaultStage(Builder.Stage.Installed)
                 .kars(toArray(installedKars))
                 .repositories(installedFeatures.isEmpty() && installedProfiles.isEmpty() && installAllFeaturesByDefault, toArray(installedRepositories))
@@ -520,47 +516,180 @@ public class AssemblyMojo extends MojoSupport {
         // Generate the assembly
         builder.generateAssembly();
 
-        // Include project classes content
+        // Include project classes content if not specified otherwise
         if (includeBuildOutputDirectory)
             IoUtils.copyDirectory(new File(project.getBuild().getOutputDirectory()), workDirectory);
 
-        // Overwrite assembly dir contents
+        // Overwrite assembly dir contents with source directory (not filtered) when directory exists
         if (sourceDirectory.exists())
             IoUtils.copyDirectory(sourceDirectory, workDirectory);
 
         // Chmod the bin/* scripts
         File[] files = new File(workDirectory, "bin").listFiles();
-        if( files!=null ) {
+        if (files != null) {
             for (File file : files) {
-                if( !file.getName().endsWith(".bat") ) {
+                if (!file.getName().endsWith(".bat")) {
                     try {
                         Files.setPosixFilePermissions(file.toPath(), PosixFilePermissions.fromString("rwxr-xr-x"));
                     } catch (Throwable ignore) {
-                        // we tried our best, perhaps the OS does not support posix file perms.
+                        // we tried our best, perhaps the OS does not support POSIX file perms.
                     }
                 }
             }
         }
     }
 
-    private Object invoke(Object object, String getter) throws MojoExecutionException {
-        try {
-            return object.getClass().getMethod(getter).invoke(object);
-        } catch (Exception e) {
-            throw new MojoExecutionException("Unable to build remote repository from " + object.toString(), e);
+    private void configureWorkDirectory() {
+        IoUtils.deleteRecursive(workDirectory);
+        workDirectory.mkdirs();
+        new File(workDirectory, "etc").mkdirs();
+        new File(workDirectory, "system").mkdirs();
+    }
+
+    /**
+     * <p>Turns direct maven dependencies into startup/boot/installed artifacts.</p>
+     * <p>{@link MavenProject#getDependencyArtifacts()} is deprecated, but we don't want (?) transitive
+     * dependencies given by {@link MavenProject#getArtifacts()}.</p>
+     */
+    @SuppressWarnings("deprecation")
+    private void processDirectMavenDependencies() {
+        for (Artifact artifact : project.getDependencyArtifacts()) {
+            Builder.Stage stage = Builder.Stage.fromMavenScope(artifact.getScope());
+            if (stage == null) {
+                continue;
+            }
+            String uri = artifactToMvn(artifact);
+            switch (getType(artifact)) {
+                case "kar":
+                    addUris(stage, uri, startupKars, bootKars, installedKars);
+                    break;
+                case "features":
+                    addUris(stage, uri, startupRepositories, bootRepositories, installedRepositories);
+                    break;
+                case "bundle":
+                    addUris(stage, uri, startupBundles, bootBundles, installedBundles);
+                    break;
+            }
         }
     }
 
-    private Object getPolicy(Object object, boolean snapshots) throws MojoExecutionException {
-        return invoke(object, "getPolicy", new Class[] { Boolean.TYPE }, new Object[] { snapshots });
+    private void addUris(Builder.Stage stage, String uri, List<String> startup, List<String> boot, List<String> installed) {
+        switch (stage) {
+            case Startup:
+                startup.add(uri);
+                break;
+            case Boot:
+                boot.add(uri);
+                break;
+            case Installed:
+                installed.add(uri);
+                break;
+        }
     }
 
-    private Object invoke(Object object, String getter, Class[] types, Object[] params) throws MojoExecutionException {
-        try {
-            return object.getClass().getMethod(getter, types).invoke(object, params);
-        } catch (Exception e) {
-            throw new MojoExecutionException("Unable to build remote repository from " + object.toString(), e);
+    /**
+     * <p>Custom distribution is created from at least one <em>startup KAR</em> and one <em>startup</em>
+     * feature. Such startup KAR + feature is called <em>framework</em>.</p>
+     *
+     * <p>We can specify one of 5 <em>frameworks</em>:<ul>
+     *     <li>framework: <code>mvn:org.apache.karaf.features/framework/VERSION/kar</code> and <code>framework</code> feature</li>
+     *     <li>framework-logback: <code>mvn:org.apache.karaf.features/framework/VERSION/kar</code> and <code>framework-logback</code> feature</li>
+     *     <li>static-framework: <code>mvn:org.apache.karaf.features/static/VERSION/kar</code> and <code>static-framework</code> feature</li>
+     *     <li>static-framework-logback: <code>mvn:org.apache.karaf.features/static/VERSION/kar</code> and <code>static-framework-logback</code> feature</li>
+     *     <li>custom: both startup KAR and startup feature has to be specified explicitly</li>
+     * </ul></p>
+     * @param builder
+     */
+    private void detectStartupKarsAndFeatures(Builder builder) {
+        boolean hasStandardKarafFrameworkKar = false;
+        boolean hasCustomFrameworkKar = false;
+        for (Iterator<String> iterator = startupKars.iterator(); iterator.hasNext(); ) {
+            String kar = iterator.next();
+            if (kar.startsWith("mvn:org.apache.karaf.features/framework/")
+                    || kar.startsWith("mvn:org.apache.karaf.features/static/")) {
+                hasStandardKarafFrameworkKar = true;
+                iterator.remove();
+                if (framework == null) {
+                    framework = kar.startsWith("mvn:org.apache.karaf.features/framework/")
+                            ? "framework" : "static-framework";
+                }
+                getLog().info("   Standard startup Karaf KAR found: " + kar);
+                builder.kars(Builder.Stage.Startup, false, kar);
+                break;
+            }
+        }
+
+        if (!hasStandardKarafFrameworkKar) {
+            if ("custom".equals(framework)) {
+                // we didn't detect standard Karaf KAR (framework or static), so we expect at least one
+                // other KAR dependency with compile scope and at least one startup feature
+                if (startupKars.isEmpty()) {
+                    throw new IllegalArgumentException("Custom KAR was declared, but there's no Maven dependency with type=kar and scope=compile." +
+                            " Please specify at least one KAR for custom assembly.");
+                }
+                if (startupFeatures.isEmpty()) {
+                    throw new IllegalArgumentException("Custom KAR was declared, but there's no startup feature declared." +
+                            " Please specify at least one startup feature defined in features XML repository inside custom startup KAR or startup repository.");
+                }
+                hasCustomFrameworkKar = true;
+                for (String startupKar : startupKars) {
+                    getLog().info("   Custom startup KAR found: " + startupKar);
+                }
+            } else if (framework == null) {
+                throw new IllegalArgumentException("Can't determine framework to use (framework, framework-logback, static-framework, static-framework-logback, custom)." +
+                        " Please specify valid \"framework\" option or add Maven dependency with \"kar\" type and \"compile\" scope for one of standard Karaf KARs.");
+            } else {
+                String realKarafVersion = Version.karafVersion();
+                String kar;
+                switch (framework) {
+                    case "framework":
+                    case "framework-logback":
+                        kar = "mvn:org.apache.karaf.features/framework/" + realKarafVersion + "/kar";
+                        break;
+                    case "static-framework":
+                    case "static-framework-logback":
+                        kar = "mvn:org.apache.karaf.features/static/" + realKarafVersion + "/kar";
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Unsupported framework: " + framework);
+                }
+                getLog().info("   Standard startup KAR implied from framework (" + framework + "): " + kar);
+                builder.kars(Builder.Stage.Startup, false, kar);
+            }
+        }
+
+        if (hasStandardKarafFrameworkKar && !startupFeatures.contains(framework)) {
+            getLog().info("   Feature " + framework + " will be added as a startup feature");
+            builder.features(Builder.Stage.Startup, framework);
+        }
+    }
+
+    private KarafPropertyEdits configurePropertyEdits() throws IOException, XMLStreamException {
+        KarafPropertyEdits edits = null;
+        if (propertyFileEdits != null) {
+            File file = new File(propertyFileEdits);
+            if (file.exists()) {
+                try (InputStream editsStream = new FileInputStream(propertyFileEdits)) {
+                    KarafPropertyInstructionsModelStaxReader kipmsr = new KarafPropertyInstructionsModelStaxReader();
+                    edits = kipmsr.read(editsStream, true);
+                }
+            }
+        }
+        return edits;
+    }
+
+    private Map<String,String> configureTranslatedUrls() {
+        Map<String, String> urls = new HashMap<>();
+        List<Artifact> artifacts = new ArrayList<>(project.getAttachedArtifacts());
+        artifacts.add(project.getArtifact());
+        for (Artifact artifact : artifacts) {
+            if (artifact.getFile() != null && artifact.getFile().exists()) {
+                String mvnUrl = artifactToMvn(artifact);
+                urls.put(mvnUrl, artifact.getFile().toURI().toString());
+            }
         }
+        urls.putAll(translatedUrls);
+        return urls;
     }
 
     private String getType(Artifact artifact) {
@@ -616,7 +745,7 @@ public class AssemblyMojo extends MojoSupport {
         return "unknown";
     }
 
-    private String artifactToMvn(Artifact artifact) throws MojoExecutionException {
+    private String artifactToMvn(Artifact artifact) {
         String uri;
 
         String groupId = artifact.getGroupId();
@@ -641,9 +770,40 @@ public class AssemblyMojo extends MojoSupport {
         return strings.toArray(new String[strings.size()]);
     }
 
+    private void setNullListsToEmpty() {
+        startupRepositories = nonNullList(startupRepositories);
+        bootRepositories = nonNullList(bootRepositories);
+        installedRepositories = nonNullList(installedRepositories);
+        blacklistedRepositories = nonNullList(blacklistedRepositories);
+        startupBundles = nonNullList(startupBundles);
+        bootBundles = nonNullList(bootBundles);
+        installedBundles = nonNullList(installedBundles);
+        blacklistedBundles = nonNullList(blacklistedBundles);
+        startupFeatures = nonNullList(startupFeatures);
+        bootFeatures = nonNullList(bootFeatures);
+        installedFeatures = nonNullList(installedFeatures);
+        blacklistedFeatures = nonNullList(blacklistedFeatures);
+        startupProfiles = nonNullList(startupProfiles);
+        bootProfiles = nonNullList(bootProfiles);
+        installedProfiles = nonNullList(installedProfiles);
+        blacklistedProfiles = nonNullList(blacklistedProfiles);
+        libraries = nonNullList(libraries);
+        profilesUris = nonNullList(profilesUris);
+    }
+
+    private void setNullMapsToEmpty() {
+        config = nonNullMap(config);
+        system = nonNullMap(system);
+        translatedUrls = nonNullMap(translatedUrls);
+    }
+
     private List<String> nonNullList(List<String> list) {
         final List<String> nonNullList = list == null ? new ArrayList<>() : list;
         return nonNullList.stream().filter(Objects::nonNull).collect(Collectors.toList());
     }
 
+    private Map<String, String> nonNullMap(Map<String, String> map) {
+        return map == null ? new LinkedHashMap<>() : map;
+    }
+
 }
diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java
index 2ff0a53..d13e786 100644
--- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java
+++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java
@@ -80,6 +80,7 @@ import org.apache.karaf.features.internal.service.StaticInstallSupport;
 import org.apache.karaf.features.internal.util.MapUtils;
 import org.apache.karaf.features.internal.util.MultiException;
 import org.apache.karaf.profile.assembly.CustomDownloadManager;
+import org.apache.karaf.tooling.utils.MavenUtil;
 import org.apache.karaf.tooling.utils.MojoSupport;
 import org.apache.karaf.tooling.utils.ReactorMavenResolver;
 import org.apache.karaf.util.config.PropertiesLoader;
@@ -167,32 +168,13 @@ public class VerifyMojo extends MojoSupport {
         }
 
         if (karafVersion == null) {
-            Properties versions = new Properties();
-            try (InputStream is = getClass().getResourceAsStream("versions.properties")) {
-                versions.load(is);
-            } catch (IOException e) {
-                throw new IllegalStateException(e);
-            }
-            karafVersion = versions.getProperty("karaf-version");
+            karafVersion = org.apache.karaf.util.Version.karafVersion();
         }
 
         Hashtable<String, String> config = new Hashtable<>();
-        StringBuilder remote = new StringBuilder();
-        for (Object obj : project.getRemoteProjectRepositories()) {
-            if (remote.length() > 0) {
-                remote.append(",");
-            }
-            remote.append(invoke(obj, "getUrl"));
-            remote.append("@id=").append(invoke(obj, "getId"));
-            if (!((Boolean) invoke(getPolicy(obj, false), "isEnabled"))) {
-                remote.append("@noreleases");
-            }
-            if ((Boolean) invoke(getPolicy(obj, true), "isEnabled")) {
-                remote.append("@snapshots");
-            }
-        }
-        getLog().info("Using repositories: " + remote.toString());
-        config.put("maven.repositories", remote.toString());
+        String remoteRepositories = MavenUtil.remoteRepositoryList(project.getRemoteProjectRepositories());
+        getLog().info("Using repositories: " + remoteRepositories);
+        config.put("maven.repositories", remoteRepositories);
         config.put("maven.localRepository", localRepo.getBasedir());
         config.put("maven.settings", mavenSession.getRequest().getUserSettingsFile().toString());
         // TODO: add more configuration bits ?
diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/MavenUtil.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/MavenUtil.java
index 4d11dd5..13fcd80 100644
--- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/MavenUtil.java
+++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/MavenUtil.java
@@ -23,6 +23,7 @@ import java.io.FileWriter;
 import java.io.IOException;
 import java.io.Writer;
 import java.util.Date;
+import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -33,6 +34,7 @@ import org.apache.maven.artifact.repository.metadata.Snapshot;
 import org.apache.maven.artifact.repository.metadata.SnapshotVersion;
 import org.apache.maven.artifact.repository.metadata.Versioning;
 import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer;
+import org.eclipse.aether.repository.RemoteRepository;
 
 /**
  * Util method for Maven manipulation (URL convert, metadata generation, etc).
@@ -41,7 +43,7 @@ public class MavenUtil {
 
     static final DefaultRepositoryLayout layout = new DefaultRepositoryLayout();
     private static final Pattern aetherPattern = Pattern.compile("([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)");
-    private static final Pattern mvnPattern = Pattern.compile("(?:(?:wrap:)|(?:blueprint:))?mvn:([^/ ]+)/([^/ ]+)/([^/\\$ ]*)(/([^/\\$ ]+)(/([^/\\$ ]+))?)?(/\\$.+)?");
+    private static final Pattern mvnPattern = Pattern.compile("(?:(?:wrap:)|(?:blueprint:))?mvn:([^/ ]+)/([^/ ]+)/([^/$ ]*)(/([^/$ ]+)(/([^/$ ]+))?)?(/\\$.+)?");
 
     /**
      * Convert PAX URL mvn format to aether coordinate format.
@@ -153,13 +155,35 @@ public class MavenUtil {
     }
     
     public static String getFileName(Artifact artifact) {
-        String name = artifact.getArtifactId() + "-" + artifact.getBaseVersion()
+        return artifact.getArtifactId() + "-" + artifact.getBaseVersion()
             + (artifact.getClassifier() != null ? "-" + artifact.getClassifier() : "") + "." + artifact.getType();
-        return name;
     }
-    
+
     public static String getDir(Artifact artifact) {
         return artifact.getGroupId().replace('.', '/') + "/" + artifact.getArtifactId() + "/" + artifact.getBaseVersion() + "/";
     }
 
+    /**
+     * Changes maven configuration of remote repositories to a list of repositories for pax-url-aether
+     * @param remoteRepositories
+     * @return
+     */
+    public static String remoteRepositoryList(List<RemoteRepository> remoteRepositories) {
+        StringBuilder remotes = new StringBuilder();
+        for (RemoteRepository rr : remoteRepositories) {
+            if (remotes.length() > 0) {
+                remotes.append(",");
+            }
+            remotes.append(rr.getUrl());
+            remotes.append("@id=").append(rr.getId());
+            if (!rr.getPolicy(false).isEnabled()) {
+                remotes.append("@noreleases");
+            }
+            if (rr.getPolicy(true).isEnabled()) {
+                remotes.append("@snapshots");
+            }
+        }
+        return remotes.toString();
+    }
+
 }

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

[karaf] 10/11: [KARAF-5468] Adjust JavaVersion enum: supportsEndorsedAndExtLibraries()

Posted by gg...@apache.org.
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 a51b572715ffc3b4484901ac6fcf2b270599397a
Author: Grzegorz Grzybek <gr...@gmail.com>
AuthorDate: Mon Nov 27 09:01:12 2017 +0100

    [KARAF-5468] Adjust JavaVersion enum: supportsEndorsedAndExtLibraries()
---
 .../main/java/org/apache/karaf/profile/assembly/Builder.java | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

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 0f7e747..c6ed79f 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
@@ -185,12 +185,14 @@ public class Builder {
      * supported versions are defined.</p>
      */
     public enum JavaVersion {
-        Java16("1.6"), Java17("1.7"), Java18("1.8"), Java9("9");
+        Java16("1.6", 1), Java17("1.7", 2), Java18("1.8", 3), Java9("9", 4);
 
         private String version;
+        private int ordinal;
 
-        JavaVersion(String version) {
+        JavaVersion(String version, int ordinal) {
             this.version = version;
+            this.ordinal = ordinal;
         }
 
         public static JavaVersion from(String version) {
@@ -203,6 +205,10 @@ public class Builder {
             }
             return v.get();
         }
+
+        public boolean supportsEndorsedAndExtLibraries() {
+            return this.ordinal < Java9.ordinal;
+        }
     }
 
     /**
@@ -1256,7 +1262,7 @@ public class Builder {
             }
             final String type = clause.getDirective(LIBRARY_CLAUSE_TYPE) != null
                     ? clause.getDirective(LIBRARY_CLAUSE_TYPE) : Library.TYPE_DEFAULT;
-            if (!javase.startsWith("1.") && (Library.TYPE_ENDORSED.equals(type) || Library.TYPE_EXTENSION.equals(type))) {
+            if (!javase.supportsEndorsedAndExtLibraries() && (Library.TYPE_ENDORSED.equals(type) || Library.TYPE_EXTENSION.equals(type))) {
                 LOGGER.warn("Ignoring library " + library + " of type " + type + " which is only supported for Java 1.8.");
                 continue;
             }

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

[karaf] 01/11: [KARAF-5376] Moving overrides and blacklist mechanisms to "features processor" service

Posted by gg...@apache.org.
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 4533176b0eff9612d696e44fcf70c2e92ef690b0
Author: Grzegorz Grzybek <gr...@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 e69656b..f14c6ae 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" <co...@karaf.apache.org>.

[karaf] 11/11: [KARAF-5376] Integrate features processor with Deployer and SubsystemResolver

Posted by gg...@apache.org.
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 c6bca029a4b4bb6c8af165d80db7f8cf9e42281c
Author: Grzegorz Grzybek <gr...@gmail.com>
AuthorDate: Sat Dec 2 20:49:40 2017 +0100

    [KARAF-5376] Integrate features processor with Deployer and SubsystemResolver
    
    After easier part of using features processor in static assembly
    builder, now it's also used by FeaturesService, Deployer and
    SubsystemResolver
    
    * SubsystemResolver takes overrides and blacklist into account during bundle download and resolution
    * Bundle override is implemented in both MAVEN and OSGI modes
    * FeaturesService uses now enums for snapshot update kind and service requirements options
    * SubsystemResolver has pulled up some methods to interfaces
    * overrides are no longer used by deployer explicitly - they're configured at model level and checked by SubsystemResolver
    * Lots of new documentation added
    * Deployer.DeploymentState has now two maps for features - by ID and by name only (mapped to list of versions of feature for given name)
    * Deployer.DeploymentRequest no longer has overrides field
    * tests added in SubsystemTest for overrides and blacklists
    * AssemblyDeployCallback no longer uses Blacklist by itself - uses features processor instead
    * More logging added to assembly Builder (we can see what's blacklisted and what's overriden)
    * Overrides are already included in distro
---
 .../karaf/features/command/InfoFeatureCommand.java |   2 +-
 .../java/org/apache/karaf/features/BundleInfo.java |  21 +-
 .../org/apache/karaf/features/FeaturePattern.java  |   5 +
 .../org/apache/karaf/features/FeaturesService.java |  64 ++++-
 .../java/org/apache/karaf/features/Library.java    |  10 +
 .../org/apache/karaf/features/LocationPattern.java |   5 +
 .../karaf/features/internal/model/Bundle.java      |   8 +-
 .../karaf/features/internal/model/Conditional.java |   2 +
 .../karaf/features/internal/model/Features.java    |  16 +-
 .../karaf/features/internal/osgi/Activator.java    |   4 +-
 .../karaf/features/internal/region/Subsystem.java  | 242 ++++++++++++++++---
 .../internal/region/SubsystemResolveContext.java   |  12 +-
 .../internal/region/SubsystemResolver.java         |  98 ++++++--
 .../SubsystemResolverCallback.java}                |  22 +-
 .../region/SubsystemResolverResolution.java        |  82 +++++++
 .../internal/region/SubsystemResolverResult.java   |  78 ++++++
 .../internal/resolver/FeatureResource.java         |  32 ++-
 .../features/internal/resolver/ResolverUtil.java   |   5 +
 .../features/internal/resolver/ResourceUtils.java  |  11 +
 .../internal/service/BundleInstallSupport.java     |  13 +-
 .../internal/service/BundleInstallSupportImpl.java |   6 +-
 .../karaf/features/internal/service/Deployer.java  | 267 +++++++++++++++------
 .../internal/service/FeaturesProcessor.java        |   7 +
 .../internal/service/FeaturesProcessorImpl.java    |  29 ++-
 .../internal/service/FeaturesServiceConfig.java    |  33 +++
 .../internal/service/FeaturesServiceImpl.java      |  20 +-
 .../karaf/features/internal/service/Overrides.java |  32 +++
 .../features/internal/service/RepositoryImpl.java  |   1 +
 .../karaf/features/internal/service/State.java     |  19 +-
 .../karaf/features/internal/util/MapUtils.java     |  29 +++
 .../internal/region/FeaturesDependenciesTest.java  |  13 +-
 .../features/internal/region/SubsystemTest.java    | 174 +++++++++++---
 .../features/internal/service/DeployerTest.java    |  48 ++--
 .../internal/service/FeaturesProcessorTest.java    |  13 +-
 .../karaf/features/internal/region/data1/d.mf      |   6 +
 .../karaf/features/internal/region/data10/a.mf     |   5 +
 .../karaf/features/internal/region/data10/b.mf     |   5 +
 .../features/internal/region/data10/features.xml   |  26 ++
 .../karaf/features/internal/region/data3/c.mf      |   5 +
 profile/pom.xml                                    |  16 +-
 .../karaf/profile/assembly/ArtifactInstaller.java  |  16 +-
 .../profile/assembly/AssemblyDeployCallback.java   |  69 ++++--
 .../org/apache/karaf/profile/assembly/Builder.java | 114 +++++----
 .../karaf/profile/assembly/ConfigInstaller.java    |   8 +-
 .../karaf/profile/impl/ProfileBuilderImpl.java     |   2 +-
 .../apache/karaf/profile/impl/ProfilesTest.java    |  15 +-
 .../java/org/apache/karaf/tooling/VerifyMojo.java  |  54 ++---
 47 files changed, 1384 insertions(+), 380 deletions(-)

diff --git a/features/command/src/main/java/org/apache/karaf/features/command/InfoFeatureCommand.java b/features/command/src/main/java/org/apache/karaf/features/command/InfoFeatureCommand.java
index 7c6a775..a8b4606 100644
--- a/features/command/src/main/java/org/apache/karaf/features/command/InfoFeatureCommand.java
+++ b/features/command/src/main/java/org/apache/karaf/features/command/InfoFeatureCommand.java
@@ -209,7 +209,7 @@ public class InfoFeatureCommand extends FeaturesCommandSupport {
                 if(startLevel > 0) {
                     sb.append(" start-level=").append(startLevel);
                 }
-                if (featureBundle.isOverriden()) {
+                if (featureBundle.isOverriden() != BundleInfo.BundleOverrideMode.NONE) {
                     sb.append(" (overriden from " + featureBundle.getOriginalLocation() + ")");
                 }
                 System.out.println(sb.toString());
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 306d8ea..7d45884 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
@@ -31,6 +31,25 @@ public interface BundleInfo extends Blacklisting {
 
     boolean isDependency();
 
-    boolean isOverriden();
+    BundleInfo.BundleOverrideMode isOverriden();
+
+    public enum BundleOverrideMode {
+        /**
+         * No override
+         */
+        NONE,
+
+        /**
+         * Compatibility with <code>${karaf.etc}/overrides.properties</code> - requires access to original and
+         * replacement bundle's headers to compare version and symbolic name.
+         */
+        OSGI,
+
+        /**
+         * Simpler option that's just static override - doesn't require accessing and checking the bundle/resource
+         * being overriden.
+         */
+        MAVEN
+    }
 
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java b/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java
index a77c4df..0fbedff 100644
--- a/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java
+++ b/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java
@@ -118,4 +118,9 @@ public class FeaturePattern {
         return match;
     }
 
+    @Override
+    public String toString() {
+        return originalId;
+    }
+
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java b/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
index da8643b..e9cc072 100644
--- a/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
+++ b/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
@@ -17,10 +17,13 @@
 package org.apache.karaf.features;
 
 import java.net.URI;
+import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.Map;
 import java.util.Set;
 
+import org.osgi.namespace.service.ServiceNamespace;
+
 /**
  * The service managing features repositories.
  */
@@ -28,20 +31,13 @@ public interface FeaturesService {
 
     String ROOT_REGION = "root";
 
-    String UPDATE_SNAPSHOTS_NONE = "none";
-    String UPDATE_SNAPSHOTS_CRC = "crc";
-    String DEFAULT_UPDATE_SNAPSHOTS = UPDATE_SNAPSHOTS_CRC;
-    String UPDATE_SNAPSHOTS_ALWAYS = "always";
+    SnapshotUpdateBehavior DEFAULT_UPDATE_SNAPSHOTS = SnapshotUpdateBehavior.Crc;
 
     String DEFAULT_FEATURE_RESOLUTION_RANGE = "${range;[====,====]}";
     String DEFAULT_BUNDLE_UPDATE_RANGE = "${range;[==,=+)}";
 
     String UPDATEABLE_URIS = "mvn:.*SNAPSHOT|(?!mvn:).*";
 
-    String SERVICE_REQUIREMENTS_DISABLE = "disable";
-    String SERVICE_REQUIREMENTS_DEFAULT = "default";
-    String SERVICE_REQUIREMENTS_ENFORCE = "enforce";
-
     int DEFAULT_DOWNLOAD_THREADS = 8;
     long DEFAULT_SCHEDULE_DELAY = 250;
     int DEFAULT_SCHEDULE_MAX_RUN = 9;
@@ -65,6 +61,58 @@ public interface FeaturesService {
     }
 
     /**
+     * Configuration options for handling requirements from {@link ServiceNamespace#SERVICE_NAMESPACE} namespace
+     */
+    enum ServiceRequirementsBehavior {
+        /** Remove and do not consider any {@link ServiceNamespace#SERVICE_NAMESPACE} requirements */
+        Disable("disable"),
+        /** Consider {@link ServiceNamespace#SERVICE_NAMESPACE} requirements only for <code>http://karaf.apache.org/xmlns/features/v1.2.1</code> XSD and below */
+        Default("default"),
+        /** Always consider {@link ServiceNamespace#SERVICE_NAMESPACE} requirements */
+        Enforce("enforce");
+
+        private String value;
+
+        ServiceRequirementsBehavior(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+
+        public static ServiceRequirementsBehavior fromString(String serviceRequirements) {
+            return Arrays.stream(values()).filter(sub -> sub.value.equalsIgnoreCase(serviceRequirements)).findFirst().orElse(Default);
+        }
+    }
+
+    /**
+     * Configuration options for checking whether update'able bundle should really be updated
+     */
+    enum SnapshotUpdateBehavior {
+        /** Never update */
+        None("none"),
+        /** Update if CRC differs */
+        Crc("crc"),
+        /** Always update */
+        Always("always");
+
+        private String value;
+
+        SnapshotUpdateBehavior(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+
+        public static SnapshotUpdateBehavior fromString(String updateSnapshots) {
+            return Arrays.stream(values()).filter(sub -> sub.value.equals(updateSnapshots)).findFirst().orElse(Crc);
+        }
+    }
+
+    /**
      * Validate repository contents.
      *
      * @param uri Repository uri.
diff --git a/features/core/src/main/java/org/apache/karaf/features/Library.java b/features/core/src/main/java/org/apache/karaf/features/Library.java
index 7ed2147..f49d938 100644
--- a/features/core/src/main/java/org/apache/karaf/features/Library.java
+++ b/features/core/src/main/java/org/apache/karaf/features/Library.java
@@ -27,8 +27,18 @@ public interface Library {
 
     String getType();
 
+    /**
+     * Whether given library's exported packages should be added to <code>org.osgi.framework.system.packages.extra</code>
+     * property in <code>${karaf.etc}/config.properties</code>.
+     * @return
+     */
     boolean isExport();
 
+    /**
+     * Whether given library's exported packages should be added to <code>org.osgi.framework.bootdelegation</code>
+     * property in <code>${karaf.etc}/config.properties</code>
+     * @return
+     */
     boolean isDelegate();
 
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java b/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
index e5c96eb..cf6f01d 100644
--- a/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
+++ b/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
@@ -204,4 +204,9 @@ public class LocationPattern {
         return match;
     }
 
+    @Override
+    public String toString() {
+        return originalUri;
+    }
+
 }
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 d0ba74f..d028eb8 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
@@ -66,7 +66,7 @@ public class Bundle implements BundleInfo {
     @XmlTransient
     private boolean blacklisted = false;
     @XmlTransient
-    private boolean overriden = false;
+    private BundleInfo.BundleOverrideMode overriden = BundleInfo.BundleOverrideMode.NONE;
 
     public Bundle() {
     }
@@ -164,13 +164,13 @@ public class Bundle implements BundleInfo {
         this.blacklisted = blacklisted;
     }
 
-    public boolean isOverriden() {
+    @Override
+    public BundleInfo.BundleOverrideMode isOverriden() {
         return overriden;
     }
 
-    public void setOverriden(boolean overriden) {
+    public void setOverriden(BundleInfo.BundleOverrideMode overriden) {
         this.overriden = overriden;
-
     }
 
     @Override
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/Conditional.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/Conditional.java
index 97e3001..d9fc5ce 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/model/Conditional.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/Conditional.java
@@ -37,6 +37,8 @@ import org.apache.karaf.features.Feature;
         })
 public class Conditional extends Content implements org.apache.karaf.features.Conditional {
 
+    // TODO: use type that really reflects <xs:element name="condition" type="tns:dependency" /> ?
+    // i.e., org.apache.karaf.features.internal.model.Dependency
     @XmlElement(name = "condition", namespace=org.apache.karaf.features.FeaturesNamespaces.URI_CURRENT)
     protected List<String> condition;
 
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/Features.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/Features.java
index af3aace..180ee1e 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/model/Features.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/Features.java
@@ -29,6 +29,8 @@ import javax.xml.bind.annotation.XmlSchemaType;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 
+import org.apache.karaf.features.Blacklisting;
+
 /**
  * <p>Root element of Feature definition. It contains optional attribute which allow
  * name of repository. This name will be used in shell to display source repository
@@ -53,7 +55,7 @@ import javax.xml.bind.annotation.XmlType;
 @XmlRootElement(name = "features", namespace=org.apache.karaf.features.FeaturesNamespaces.URI_CURRENT)
 @XmlAccessorType(XmlAccessType.FIELD)
 @XmlType(name = "features", propOrder = {"repository", "resourceRepository", "feature"})
-public class Features {
+public class Features implements Blacklisting {
 
     @XmlSchemaType(name = "anyURI")
     @XmlElement(name = "repository", namespace=org.apache.karaf.features.FeaturesNamespaces.URI_CURRENT)
@@ -67,6 +69,8 @@ public class Features {
     protected String name;
     @XmlTransient
     private String namespace;
+    @XmlTransient
+    private boolean blacklisted;
 
     /**
      * <p>Get the value of the repository property.</p>
@@ -196,4 +200,14 @@ public class Features {
     public String getNamespace() {
         return namespace;
     }
+
+    @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/osgi/Activator.java b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
index ad6d940..a950f90 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
@@ -232,13 +232,13 @@ public class Activator extends BaseActivator {
             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),
+            getString("updateSnapshots", FeaturesService.DEFAULT_UPDATE_SNAPSHOTS.getValue()),
             getInt("downloadThreads", FeaturesService.DEFAULT_DOWNLOAD_THREADS),
             getLong("scheduleDelay", FeaturesService.DEFAULT_SCHEDULE_DELAY),
             getInt("scheduleMaxRun", FeaturesService.DEFAULT_SCHEDULE_MAX_RUN),
             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));
+            getString("serviceRequirements", FeaturesService.ServiceRequirementsBehavior.Default.getValue()));
     }
 
     private StateStorage createStateStorage() {
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 cfd295e..523e838 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
@@ -22,6 +22,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -76,6 +77,9 @@ import static org.osgi.framework.namespace.IdentityNamespace.CAPABILITY_VERSION_
 import static org.osgi.framework.namespace.IdentityNamespace.IDENTITY_NAMESPACE;
 import static org.osgi.resource.Namespace.REQUIREMENT_FILTER_DIRECTIVE;
 
+/**
+ * A {@link Resource} representing ...
+ */
 public class Subsystem extends ResourceImpl {
 
     private static final String ALL_FILTER = "(|(!(all=*))(all=*))";
@@ -98,20 +102,49 @@ public class Subsystem extends ResourceImpl {
                     IDENTITY_NAMESPACE,
                     Collections.singleton(SUBSYSTEM_FILTER));
 
+    // name of the subsystem: region or region#feature[-version]
     private final String name;
+    // works only with feature scoping. region subsystems by default accept deps
     private final boolean acceptDependencies;
+    // parent Subsystem for child subsystems representing child regions or regions' features
     private final Subsystem parent;
+    // feature for Subsystem representing a feature
     private final Feature feature;
+
     private final boolean mandatory;
+
     private final List<Subsystem> children = new ArrayList<>();
+
+    // a set of filters applied when child subsystem needs capabilities from parent subsystem
     private final Map<String, Set<String>> importPolicy;
+    // a set of filters applied when parent subsystem needs capabilities from child subsystem
     private final Map<String, Set<String>> exportPolicy;
+
+    // contains subsystems representing features of this region, child subsystems for child regions, system resources(?),
+    // bundle resources added explicitly as reqs for this Subsystem, feature resources for subsystems representing
+    // features, ...
     private final List<Resource> installable = new ArrayList<>();
+
+    // mapping from "symbolic-name|version" to a DependencyInfo wrapping a Resource
+    // <bundle dependency="false"> are collected directly in feature's subsystem
+    // <bundle dependency="true"> are collected in first parent subsystem of feature or in subsystem of scoped feature
     private final Map<String, DependencyInfo> dependencies = new HashMap<>();
+    // non-mandatory dependant features (<feature>/<feature>) collected from current and child subsystems representing
+    // features (unless some subsystem for feature has <scoping acceptDependencies="true">)
     private final List<Requirement> dependentFeatures = new ArrayList<>();
 
+    // direct bundle URI dependencies - not added by FeaturesService, but used in startup stage of assembly builder
+    // these bundles will be downloaded
     private final List<String> bundles = new ArrayList<>();
 
+    /**
+     * <p>Constructs root subsystem {@link Resource} for {@link FeaturesService#ROOT_REGION} that imports/exports only
+     * caps/reqs with <code>(type=karaf.subsystem)</code></p>
+     * <p>Root subsystem by default accepts dependencies - will gather dependant features of child feature subsystems,
+     * effectively _flattening_ the set of features within single region's subsystem.</p>
+     *
+     * @param name
+     */
     public Subsystem(String name) {
         super(name, TYPE_SUBSYSTEM, Version.emptyVersion);
         this.name = name;
@@ -123,6 +156,16 @@ public class Subsystem extends ResourceImpl {
         this.mandatory = true;
     }
 
+    /**
+     * <p>Constructs subsystem for a feature that either imports/exports all caps or (see {@link Feature#getScoping()})
+     * has configurable import/export policy + <code>(|(type=karaf.subsystem)(type=karaf.feature))</code> filter in
+     * {@link org.osgi.framework.namespace.IdentityNamespace#IDENTITY_NAMESPACE}</p>
+     * <p>Such subsystem requires <code>type=karaf.feature; osgi.identity=feature-name[; version=feature-version]</code></p>
+     * @param name
+     * @param feature
+     * @param parent
+     * @param mandatory
+     */
     public Subsystem(String name, Feature feature, Subsystem parent, boolean mandatory) {
         super(name, TYPE_SUBSYSTEM, Version.emptyVersion);
         this.name = name;
@@ -146,6 +189,14 @@ public class Subsystem extends ResourceImpl {
                 new VersionRange(VersionTable.getVersion(feature.getVersion()), true));
     }
 
+    /**
+     * <p>Constructs child subsystem {@link Resource} for {@link FeaturesService#ROOT_REGION}'s child
+     * that imports all caps and exports only caps with <code>(type=karaf.subsystem)</code></p>
+     * @param name
+     * @param parent
+     * @param acceptDependencies
+     * @param mandatory
+     */
     public Subsystem(String name, Subsystem parent, boolean acceptDependencies, boolean mandatory) {
         super(name, TYPE_SUBSYSTEM, Version.emptyVersion);
         this.name = name;
@@ -198,6 +249,13 @@ public class Subsystem extends ResourceImpl {
         return feature;
     }
 
+    /**
+     * Create child subsystem for this subsystem. Child will become parent's mandatory requirement to force its resolution.
+     *
+     * @param name
+     * @param acceptDependencies
+     * @return
+     */
     public Subsystem createSubsystem(String name, boolean acceptDependencies) {
         if (feature != null) {
             throw new UnsupportedOperationException("Can not create application subsystems inside a feature subsystem");
@@ -269,15 +327,23 @@ public class Subsystem extends ResourceImpl {
     }
 
     @SuppressWarnings("InfiniteLoopStatement")
-    public void build(Collection<Feature> features) throws Exception {
-        doBuild(features, true);
+    public void build(Map<String, List<Feature>> allFeatures) throws Exception {
+        doBuild(allFeatures, true);
     }
 
-    private void doBuild(Collection<Feature> features, boolean mandatory) throws Exception {
+    /**
+     *
+     * @param allFeatures
+     * @param mandatory
+     * @throws Exception
+     */
+    private void doBuild(Map<String, List<Feature>> allFeatures, boolean mandatory) throws Exception {
         for (Subsystem child : children) {
-            child.doBuild(features, true);
+            child.doBuild(allFeatures, true);
         }
         if (feature != null) {
+            // each dependant feature becomes a non-mandatory (why?) requirement of first parent that
+            // accepts dependencies
             for (Dependency dep : feature.getDependencies()) {
                 Subsystem ss = this;
                 while (!ss.isAcceptDependencies()) {
@@ -285,13 +351,14 @@ public class Subsystem extends ResourceImpl {
                 }
                 ss.requireFeature(dep.getName(), dep.getVersion(), false);
             }
+            // each conditional feature becomes a child subsystem of this feature's subsystem
             for (Conditional cond : feature.getConditional()) {
                 Feature fcond = cond.asFeature();
                 String ssName = this.name + "#" + (fcond.hasVersion() ? fcond.getName() + "-" + fcond.getVersion() : fcond.getName());
                 Subsystem fs = getChild(ssName);
                 if (fs == null) {
                     fs = new Subsystem(ssName, fcond, this, true);
-                    fs.doBuild(features, false);
+                    fs.doBuild(allFeatures, false);
                     installable.add(fs);
                     children.add(fs);
                 }
@@ -305,20 +372,21 @@ public class Subsystem extends ResourceImpl {
             if (requirements.isEmpty()) {
                 break;
             }
+            // for each feature requirement on this subsystem (osgi.identity;type=karaf.feature), we create a
+            // Subsystem representing mandatory feature.
             for (Requirement requirement : requirements) {
                 String name = (String) requirement.getAttributes().get(IDENTITY_NAMESPACE);
                 String type = (String) requirement.getAttributes().get(CAPABILITY_TYPE_ATTRIBUTE);
                 VersionRange range = (VersionRange) requirement.getAttributes().get(CAPABILITY_VERSION_ATTRIBUTE);
-                if (TYPE_FEATURE.equals(type)) {
-                    for (Feature feature : features) {
-                        if (feature.getName().equals(name)
-                                && (range == null || range.contains(VersionTable.getVersion(feature.getVersion())))) {
+                if (TYPE_FEATURE.equals(type) && allFeatures.containsKey(name)) {
+                    for (Feature feature : allFeatures.get(name)) {
+                        if (range == null || range.contains(VersionTable.getVersion(feature.getVersion()))) {
                             if (feature != this.feature) {
                                 String ssName = this.name + "#" + (feature.hasVersion() ? feature.getName() + "-" + feature.getVersion() : feature.getName());
                                 Subsystem fs = getChild(ssName);
                                 if (fs == null) {
                                     fs = new Subsystem(ssName, feature, this, mandatory && !SubsystemResolveContext.isOptional(requirement));
-                                    fs.build(features);
+                                    fs.build(allFeatures);
                                     installable.add(fs);
                                     children.add(fs);
                                 }
@@ -347,10 +415,12 @@ public class Subsystem extends ResourceImpl {
                 String[] p = prereq.split("/");
                 if (feature.getName().equals(p[0])
                         && VersionRange.parseVersionRange(p[1]).contains(Version.parseVersion(feature.getVersion()))) {
+                    // our feature is already among prerequisites, so ...
                     match = true;
                     break;
                 }
             }
+            // ... we won't be adding its prerequisites - they'll be handled after another PartialDeploymentException
             if (!match) {
                 for (Dependency dep : feature.getDependencies()) {
                     if (dep.isPrerequisite()) {
@@ -361,21 +431,32 @@ public class Subsystem extends ResourceImpl {
         }
     }
 
+    /**
+     * Downloads bundles for all the features in current and child subsystems. But also collects bundles
+     * as {@link DependencyInfo}.
+     * @param manager
+     * @param featureResolutionRange
+     * @param serviceRequirements
+     * @param repos
+     * @throws Exception
+     */
     @SuppressWarnings("InfiniteLoopStatement")
     public void downloadBundles(DownloadManager manager,
-                                Set<String> overrides,
                                 String featureResolutionRange,
-                                final String serviceRequirements,
-                                RepositoryManager repos) throws Exception {
+                                final FeaturesService.ServiceRequirementsBehavior serviceRequirements,
+                                RepositoryManager repos,
+                                SubsystemResolverCallback callback) throws Exception {
         for (Subsystem child : children) {
-            child.downloadBundles(manager, overrides, featureResolutionRange, serviceRequirements, repos);
+            child.downloadBundles(manager, featureResolutionRange, serviceRequirements, repos, callback);
         }
-        final Map<String, ResourceImpl> bundles = new ConcurrentHashMap<>();
-        final Downloader downloader = manager.createDownloader();
+
+        // collect BundleInfos for given feature - both direct <feature>/<bundle>s and <feature>/<conditional>/<bundle>s
         final Map<BundleInfo, Conditional> infos = new HashMap<>();
+        final Downloader downloader = manager.createDownloader();
         if (feature != null) {
             for (Conditional cond : feature.getConditional()) {
                 for (final BundleInfo bi : cond.getBundles()) {
+                    // bundles from conditional features will be added as non-mandatory requirements
                     infos.put(bi, cond);
                 }
             }
@@ -383,36 +464,62 @@ public class Subsystem extends ResourceImpl {
                 infos.put(bi, null);
             }
         }
-        boolean removeServiceRequirements;
-        if (FeaturesService.SERVICE_REQUIREMENTS_DISABLE.equals(serviceRequirements)) {
-            removeServiceRequirements = true;
-        } else if (feature != null && FeaturesService.SERVICE_REQUIREMENTS_DEFAULT.equals(serviceRequirements)) {
-            removeServiceRequirements = FeaturesNamespaces.URI_1_0_0.equals(feature.getNamespace())
-                                     || FeaturesNamespaces.URI_1_1_0.equals(feature.getNamespace())
-                                     || FeaturesNamespaces.URI_1_2_0.equals(feature.getNamespace())
-                                     || FeaturesNamespaces.URI_1_2_1.equals(feature.getNamespace());
-        } else {
-            removeServiceRequirements = false;
+
+        // features model doesn't have blacklisted entries removed, but marked as blacklisted - we now don't have
+        // to download them
+        //infos.keySet().removeIf(Blacklisting::isBlacklisted);
+        for (Iterator<BundleInfo> iterator = infos.keySet().iterator(); iterator.hasNext(); ) {
+            BundleInfo bi = iterator.next();
+            if (bi.isBlacklisted()) {
+                iterator.remove();
+                if (callback != null) {
+                    callback.bundleBlacklisted(bi);
+                }
+            }
         }
+
+        // all downloaded bundles
+        final Map<String, ResourceImpl> bundles = new ConcurrentHashMap<>();
+        // resources for locations that were overriden in OSGi mode - to check whether the override should actually
+        // take place, by checking resource's headers
+        final Map<String, ResourceImpl> overrides = new ConcurrentHashMap<>();
+
+        boolean removeServiceRequirements = serviceRequirementsBehavior(feature, serviceRequirements);
+
+        // download collected BundleInfo locations
         for (Map.Entry<BundleInfo, Conditional> entry : infos.entrySet()) {
             final BundleInfo bi = entry.getKey();
             final String loc = bi.getLocation();
             downloader.download(loc, provider -> {
-                bundles.put(loc, createResource(loc, getMetadata(provider), removeServiceRequirements));
+                // always download location (could be overriden)
+                ResourceImpl resource = createResource(loc, getMetadata(provider), removeServiceRequirements);
+                bundles.put(loc, resource);
+
+                if (bi.isOverriden() == BundleInfo.BundleOverrideMode.OSGI) {
+                    // also download original from original bundle URI to check if we should override by comparing
+                    // symbolic name - requires MANIFEST.MF header access. If there should be no override, we'll get
+                    // back to original URI
+                    downloader.download(bi.getOriginalLocation(), provider2 -> {
+                        ResourceImpl originalResource = createResource(bi.getOriginalLocation(),
+                                getMetadata(provider2), removeServiceRequirements);
+                        bundles.put(bi.getOriginalLocation(), originalResource);
+                        // an entry in overrides map means that given location was overriden
+                        overrides.put(loc, originalResource);
+                    });
+                }
             });
         }
+        // download direct bundle: requirements - without consulting overrides
         for (Clause bundle : Parser.parseClauses(this.bundles.toArray(new String[this.bundles.size()]))) {
             final String loc = bundle.getName();
             downloader.download(loc, provider -> {
                 bundles.put(loc, createResource(loc, getMetadata(provider), removeServiceRequirements));
             });
         }
-        for (String override : overrides) {
-            final String loc = Overrides.extractUrl(override);
-            downloader.download(loc, provider -> {
-                bundles.put(loc, createResource(loc, getMetadata(provider), removeServiceRequirements));
-            });
-        }
+        // we *don't* have to download overrides separately - they're already taken into account from processed model
+
+        // download additional libraries - only exported, so they're capabilities are taken into account during
+        // resolution process
         if (feature != null) {
             for (Library library : feature.getLibraries()) {
                 if (library.isExport()) {
@@ -424,18 +531,25 @@ public class Subsystem extends ResourceImpl {
             }
         }
         downloader.await();
+
+        // opposite to what we had before. Currently bundles are already overriden at model level, but
+        // as we finally have access to headers, we can compare symbolic names and if override mode is OSGi, then
+        // we can restore original resource if there should be no override.
         Overrides.override(bundles, overrides);
+
         if (feature != null) {
             // Add conditionals
             Map<Conditional, Resource> resConds = new HashMap<>();
             for (Conditional cond : feature.getConditional()) {
                 FeatureResource resCond = FeatureResource.build(feature, cond, featureResolutionRange, bundles);
+                // feature's subsystem will optionally require conditional feature resource
                 addIdentityRequirement(this, resCond, false);
+                // but it's a mandatory requirement in other way
                 addIdentityRequirement(resCond, this, true);
                 installable.add(resCond);
                 resConds.put(cond, resCond);
             }
-            // Add features
+            // Add features and make it require given subsystem that represents logical feature requirement
             FeatureResource resFeature = FeatureResource.build(feature, featureResolutionRange, bundles);
             addIdentityRequirement(resFeature, this);
             installable.add(resFeature);
@@ -447,6 +561,7 @@ public class Subsystem extends ResourceImpl {
                 ResourceImpl res = bundles.get(loc);
                 int sl = bi.getStartLevel() <= 0 ? feature.getStartLevel() : bi.getStartLevel();
                 if (cond != null) {
+                    // bundle of conditional feature will have mandatory requirement on it
                     addIdentityRequirement(res, resConds.get(cond), true);
                 }
                 boolean mandatory = !bi.isDependency() && cond == null;
@@ -486,16 +601,39 @@ public class Subsystem extends ResourceImpl {
                 addDependency(bundles.get(loc), false, start, startLevel, blacklisted);
             } else {
                 doAddDependency(bundles.get(loc), true, start, startLevel, blacklisted);
+                // non dependency bundle will be added as osgi.identity req on type=osgi.bundle
                 addIdentityRequirement(this, bundles.get(loc));
             }
         }
         // Compute dependencies
         for (DependencyInfo info : dependencies.values()) {
             installable.add(info.resource);
+            // bundle resource will have a requirement on its feature's subsystem too
+            // when bundle is declared with dependency="true", it will have a requirement on its region's subsystem
             addIdentityRequirement(info.resource, this, info.mandatory);
         }
     }
 
+    /**
+     * How to handle requirements from {@link org.osgi.namespace.service.ServiceNamespace#SERVICE_NAMESPACE} for
+     * given feature.
+     * @param feature
+     * @param serviceRequirements
+     * @return
+     */
+    private boolean serviceRequirementsBehavior(Feature feature, FeaturesService.ServiceRequirementsBehavior serviceRequirements) {
+        if (FeaturesService.ServiceRequirementsBehavior.Disable == serviceRequirements) {
+            return true;
+        } else if (feature != null && FeaturesService.ServiceRequirementsBehavior.Default == serviceRequirements) {
+            return FeaturesNamespaces.URI_1_0_0.equals(feature.getNamespace())
+                    || FeaturesNamespaces.URI_1_1_0.equals(feature.getNamespace())
+                    || FeaturesNamespaces.URI_1_2_0.equals(feature.getNamespace())
+                    || FeaturesNamespaces.URI_1_2_1.equals(feature.getNamespace());
+        } else {
+            return false;
+        }
+    }
+
     ResourceImpl cloneResource(Resource resource) {
         ResourceImpl res = new ResourceImpl();
         for (Capability cap : resource.getCapabilities(null)) {
@@ -536,6 +674,16 @@ public class Subsystem extends ResourceImpl {
         throw new IllegalArgumentException("Resource " + provider.getUrl() + " does not contain a manifest");
     }
 
+    /**
+     * Adds a {@link Resource} as dependency if this subsystem {@link Subsystem#isAcceptDependencies() accepts dependencies},
+     * otherwise, the dependency is added to parent subsystem, effectively searching for first parent subsystem representing
+     * region or scoped feature.
+     * @param resource
+     * @param mandatory
+     * @param start
+     * @param startLevel
+     * @param blacklisted
+     */
     void addDependency(ResourceImpl resource, boolean mandatory, boolean start, int startLevel, boolean blacklisted) {
         if (isAcceptDependencies()) {
             doAddDependency(resource, mandatory, start, startLevel, blacklisted);
@@ -544,12 +692,27 @@ public class Subsystem extends ResourceImpl {
         }
     }
 
+    /**
+     * Adds a {@link Resource} to this subsystem
+     * @param resource
+     * @param mandatory
+     * @param start
+     * @param startLevel
+     * @param blacklisted
+     */
     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, blacklisted);
         dependencies.merge(id, info, this::merge);
     }
 
+    /**
+     * Merges two dependencies by taking lower start level, stronger <code>mandatory</code> option and stronger
+     * <code>start</code> option.
+     * @param di1
+     * @param di2
+     * @return
+     */
     private DependencyInfo merge(DependencyInfo di1, DependencyInfo di2) {
         DependencyInfo info = new DependencyInfo();
         if (di1.resource != di2.resource) {
@@ -561,7 +724,7 @@ public class Subsystem extends ResourceImpl {
                 info.resource = di2.resource;
             } else {
                 String id = ResolverUtil.getSymbolicName(di1.resource) + "/" + ResolverUtil.getVersion(di1.resource);
-                throw new IllegalStateException("Resource " + id + " is duplicated on subsystem " + this.toString() + ". First resource requires " + (r1 != null ? r1 : "nothing") + " while the second requires " + (r2 != null ? r2 : "nothing"));
+                throw new IllegalStateException("Resource " + id + " is duplicated on subsystem " + this.toString() + ". First resource requires " + r1 + " while the second requires " + r2);
             }
         } else {
             info.resource = di1.resource;
@@ -585,13 +748,16 @@ public class Subsystem extends ResourceImpl {
         return null;
     }
 
+    /**
+     * TODOCUMENT: More generic than just {@link BundleInfo}
+     */
     class DependencyInfo implements BundleInfo {
         ResourceImpl resource;
         boolean mandatory;
         boolean start;
         int startLevel;
         boolean blacklisted;
-        boolean overriden;
+        BundleInfo.BundleOverrideMode overriden;
 
         public DependencyInfo() {
         }
@@ -636,11 +802,11 @@ public class Subsystem extends ResourceImpl {
         }
 
         @Override
-        public boolean isOverriden() {
+        public BundleInfo.BundleOverrideMode isOverriden() {
             return overriden;
         }
 
-        public void setOverriden(boolean overriden) {
+        public void setOverriden(BundleInfo.BundleOverrideMode overriden) {
             this.overriden = overriden;
         }
 
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java
index c162b81..6bb1166 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java
@@ -38,7 +38,6 @@ import org.apache.karaf.features.internal.resolver.ResourceImpl;
 import org.eclipse.equinox.region.Region;
 import org.eclipse.equinox.region.RegionDigraph;
 import org.eclipse.equinox.region.RegionFilter;
-import org.osgi.framework.BundleException;
 import org.osgi.framework.namespace.PackageNamespace;
 import org.osgi.framework.wiring.BundleRevision;
 import org.osgi.namespace.service.ServiceNamespace;
@@ -72,9 +71,9 @@ public class SubsystemResolveContext extends ResolveContext {
     private final Repository repository;
     private final Repository globalRepository;
     private final Downloader downloader;
-    private final String serviceRequirements;
+    private final FeaturesService.ServiceRequirementsBehavior serviceRequirements;
 
-    public SubsystemResolveContext(Subsystem root, RegionDigraph digraph, Repository globalRepository, Downloader downloader, String serviceRequirements) throws BundleException {
+    public SubsystemResolveContext(Subsystem root, RegionDigraph digraph, Repository globalRepository, Downloader downloader, FeaturesService.ServiceRequirementsBehavior serviceRequirements) {
         this.root = root;
         this.globalRepository = globalRepository != null ? new SubsystemRepository(globalRepository) : null;
         this.downloader = downloader;
@@ -159,6 +158,11 @@ public class SubsystemResolveContext extends ResolveContext {
         return PackageNamespace.RESOLUTION_DYNAMIC.equals(resolution);
     }
 
+    /**
+     * {@link #resToSub} will quickly map all {@link Subsystem#getInstallable() installable resources} to their
+     * {@link Subsystem}
+     * @param subsystem
+     */
     void prepare(Subsystem subsystem) {
         resToSub.put(subsystem, subsystem);
         for (Resource res : subsystem.getInstallable()) {
@@ -275,7 +279,7 @@ public class SubsystemResolveContext extends ResolveContext {
     @Override
     public boolean isEffective(Requirement requirement) {
         boolean isServiceReq = ServiceNamespace.SERVICE_NAMESPACE.equals(requirement.getNamespace());
-        return !(isServiceReq && FeaturesService.SERVICE_REQUIREMENTS_DISABLE.equals(serviceRequirements));
+        return !(isServiceReq && FeaturesService.ServiceRequirementsBehavior.Disable == serviceRequirements);
     }
 
     @Override
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolver.java b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolver.java
index d9ec573..87de2b8 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolver.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolver.java
@@ -22,10 +22,12 @@ import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.nio.file.StandardOpenOption;
 import java.util.*;
+import java.util.stream.Collectors;
 
 import org.apache.felix.utils.collections.DictionaryAsMap;
 import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.FeaturesService;
 import org.apache.karaf.features.internal.download.DownloadManager;
 import org.apache.karaf.features.internal.download.Downloader;
 import org.apache.karaf.features.internal.download.StreamProvider;
@@ -66,7 +68,7 @@ import static org.osgi.framework.namespace.IdentityNamespace.IDENTITY_NAMESPACE;
 import static org.osgi.framework.namespace.IdentityNamespace.TYPE_BUNDLE;
 import static org.osgi.framework.namespace.IdentityNamespace.TYPE_FRAGMENT;
 
-public class SubsystemResolver {
+public class SubsystemResolver implements SubsystemResolverResolution, SubsystemResolverResult {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(SubsystemResolver.class);
 
@@ -86,17 +88,30 @@ public class SubsystemResolver {
     private RegionDigraph flatDigraph;
     private Map<String, Map<String, BundleInfo>> bundleInfos;
 
+    private SubsystemResolverCallback callback;
+
     public SubsystemResolver(Resolver resolver, DownloadManager manager) {
         this.resolver = resolver;
         this.manager = manager;
     }
 
+    public void setDeployCallback(SubsystemResolverCallback callback) {
+        this.callback = callback;
+    }
+
+    @Override
     public void prepare(
-            Collection<Feature> allFeatures,
+            Map<String, List<Feature>> allFeatures,
             Map<String, Set<String>> requirements,
             Map<String, Set<BundleRevision>> system
     ) throws Exception {
-        // Build subsystems on the fly
+        // #1. Build subsystems on the fly
+        //  - regions use hierarchical names with root region called "root" and child regions named "root/child",
+        //    "root/child/grandchild", etc.
+        //  - there can be only one root region and even if equinox Regions can be configured as digraph, only tree
+        //    structure is used
+        //  - each region will have corresponding Subsystem created and (being an OSGi Resource), will _require_
+        //    related requirements. Each region's subsystem will also _require_ all child subsystems
         for (Map.Entry<String, Set<String>> entry : requirements.entrySet()) {
             String[] parts = entry.getKey().split("/");
             if (root == null) {
@@ -106,9 +121,14 @@ public class SubsystemResolver {
             }
             Subsystem ss = root;
             for (int i = 1; i < parts.length; i++) {
-                ss = getOrCreateChild(ss, parts[i]);
+                String childName = Arrays.stream(Arrays.copyOfRange(parts, 0, i + 1)).collect(Collectors.joining("/"));
+                ss = getOrCreateChild(ss, childName, parts[i]);
             }
             for (String requirement : entry.getValue()) {
+                // #1a. each "[feature:]*" and "requirement:*" requirements are added directly as resource requirements:
+                //  - feature: ns=osgi.identity, 'osgi.identity=f1; type=karaf.feature; filter:="(&(osgi.identity=f1)(type=karaf.feature))"'
+                //  - requirement: as-is
+                //  - bundle: added only as downloadable bundle - used only by assembly builder
                 ss.require(requirement);
             }
         }
@@ -116,10 +136,18 @@ public class SubsystemResolver {
             return;
         }
 
-        // Pre-resolve
+        // #2. Pre-resolve
+        //  - for each region's subsystem X, feature requirements are changed into child subsystems of X
+        //  - for each feature, any dependant features (<feature>/<feature>) will become non-mandatory (why?)
+        //    child subsystem of the same region's subsystem as original feature
+        //  - for each feature, any conditional (<feature>/<conditional>) will become mandatory (why?)
+        //    child subsystem of the original feature's subsystem
         root.build(allFeatures);
 
-        // Add system resources
+        // #3. Add system resources
+        //  - from all unmanaged bundles we'll gather Provide-Capability headers' clauses in "osgi.service" namespace
+        //    and Export-Service headers
+        //  - these capabilities will be added to "dummy" Resource added as o.a.k.features.internal.region.Subsystem.installable
         BundleRevision sysBundleRev = null;
         boolean hasEeCap = false;
         for (Map.Entry<String, Set<BundleRevision>> entry : system.entrySet()) {
@@ -164,17 +192,18 @@ public class SubsystemResolver {
         }
     }
 
-    public Set<String> collectPrerequisites() throws Exception {
+    @Override
+    public Set<String> collectPrerequisites() {
         if (root != null) {
             return root.collectPrerequisites();
         }
         return new HashSet<>();
     }
 
+    @Override
     public Map<Resource, List<Wire>> resolve(
-            Set<String> overrides,
             String featureResolutionRange,
-            String serviceRequirements,
+            FeaturesService.ServiceRequirementsBehavior serviceRequirements,
             final Repository globalRepository,
             String outputFile) throws Exception {
 
@@ -183,8 +212,7 @@ public class SubsystemResolver {
         }
 
         // Download bundles
-        RepositoryManager repos = new RepositoryManager();
-        root.downloadBundles(manager, overrides, featureResolutionRange, serviceRequirements, repos);
+        root.downloadBundles(manager, featureResolutionRange, serviceRequirements, new RepositoryManager(), callback);
 
         // Populate digraph and resolve
         digraph = new StandardRegionDigraph(null, null);
@@ -199,6 +227,7 @@ public class SubsystemResolver {
             }
             json.put("repository", toJson(context.getRepository()));
             try {
+                // this is where the magic happens...
                 wiring = resolver.resolve(context);
                 json.put("success", "true");
                 json.put("wiring", toJson(wiring));
@@ -215,6 +244,7 @@ public class SubsystemResolver {
                 }
             }
         } else {
+            // this is where the magic happens...
             wiring = resolver.resolve(context);
         }
         downloader.await();
@@ -294,6 +324,7 @@ public class SubsystemResolver {
         return obj;
     }
 
+    @Override
     public Map<String, Map<String, BundleInfo>> getBundleInfos() {
         if (bundleInfos == null) {
             bundleInfos = new HashMap<>();
@@ -312,14 +343,17 @@ public class SubsystemResolver {
         }
     }
 
+    @Override
     public Map<String, StreamProvider> getProviders() {
         return manager.getProviders();
     }
 
+    @Override
     public Map<Resource, List<Wire>> getWiring() {
         return wiring;
     }
 
+    @Override
     public RegionDigraph getFlatDigraph() throws BundleException, InvalidSyntaxException {
         if (flatDigraph == null) {
             flatDigraph = new StandardRegionDigraph(null, null);
@@ -355,6 +389,10 @@ public class SubsystemResolver {
         return flatDigraph;
     }
 
+    /**
+     * A mapping from subsystem, to parent subsystem representing a region or {@link Feature#getScoping() scoped feature}.
+     * @return
+     */
     public Map<String, String> getFlatSubsystemsMap() {
         if (flatSubsystemsMap == null) {
             flatSubsystemsMap = new HashMap<>();
@@ -363,6 +401,7 @@ public class SubsystemResolver {
         return flatSubsystemsMap;
     }
 
+    @Override
     public Map<String, Set<Resource>> getBundlesPerRegions() {
         if (bundlesPerRegions == null) {
             bundlesPerRegions = invert(getBundles());
@@ -371,7 +410,6 @@ public class SubsystemResolver {
     }
 
     /**
-     *
      * @return map of bundles and the region they are deployed in
      */
     public Map<Resource, String> getBundles() {
@@ -386,6 +424,7 @@ public class SubsystemResolver {
         return bundles;
     }
 
+    @Override
     public Map<String, Set<Resource>> getFeaturesPerRegions() {
         if (featuresPerRegions == null) {
             featuresPerRegions = invert(getFeatures());
@@ -393,6 +432,9 @@ public class SubsystemResolver {
         return featuresPerRegions;
     }
 
+    /**
+     * @return map of features and the region they are deployed in
+     */
     public Map<Resource, String> getFeatures() {
         if (features == null) {
             SimpleFilter sf = createFilter(IDENTITY_NAMESPACE, "*",
@@ -403,7 +445,7 @@ public class SubsystemResolver {
     }
 
     /**
-     *
+     * Returns a mapping for resources that match given filter, to a subsystem that represents region or scoped feature.
      * @param resourceFilter
      * @return map from resource to region name
      */
@@ -503,6 +545,12 @@ public class SubsystemResolver {
 
     }
 
+    /**
+     * Collect a mapping from every subsystem to their first parent subsystem that is not <em>flat</em>, i.e.,
+     * is not a subsystem for feature or represents a feature with scoping.
+     * @param subsystem
+     * @param toFlatten
+     */
     private void findSubsystemsToFlatten(Subsystem subsystem, Map<String, String> toFlatten) {
         Subsystem nonFlat = subsystem;
         while (isFlat(nonFlat)) {
@@ -516,6 +564,11 @@ public class SubsystemResolver {
         }
     }
 
+    /**
+     * Subsystem is <em>flat</em> if it represents a feature and doesn't declare scoping
+     * @param subsystem
+     * @return
+     */
     private static boolean isFlat(Subsystem subsystem) {
         if (subsystem == null || subsystem.getFeature() == null) {
             return false;
@@ -523,14 +576,27 @@ public class SubsystemResolver {
         return subsystem.getFeature() != null && subsystem.getFeature().getScoping() == null;
     }
 
-    private static Subsystem getOrCreateChild(Subsystem ss, String name) {
-        Subsystem child = ss.getChild(name);
-        return child != null ? child : ss.createSubsystem(name, true);
+    private static Subsystem getOrCreateChild(Subsystem ss, String childName, String newName) {
+        Subsystem child = ss.getChild(childName);
+        return child != null ? child : ss.createSubsystem(newName, true);
     }
 
+    /**
+     * <p>Fills {@link RegionDigraph} using information in populated {@link Subsystem}. Each subsystem, not only
+     * subsystem representing a region, will be mapped to distinct region. We have subsystems created for:<ul>
+     *     <li>regions: "region", ..., "region/sub/region"</li>
+     *     <li>features: "region/sub/region#fx-version", ..., "region/sub/region#fz-version"</li>
+     *     <li>conditional features: "region/sub/region#fx#fx-condition-fy-version", ...</li>
+     * </ul></p>
+     * @param digraph
+     * @param subsystem
+     * @throws BundleException
+     * @throws InvalidSyntaxException
+     */
     private void populateDigraph(RegionDigraph digraph, Subsystem subsystem) throws BundleException, InvalidSyntaxException {
         Region region = digraph.createRegion(subsystem.getName());
         if (subsystem.getParent() != null) {
+            // there's always a parent, since we're traversing breadth-first
             Region parent = digraph.getRegion(subsystem.getParent().getName());
             digraph.connect(region, createRegionFilterBuilder(digraph, subsystem.getImportPolicy()).build(), parent);
             digraph.connect(parent, createRegionFilterBuilder(digraph, subsystem.getExportPolicy()).build(), region);
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/region/SubsystemResolverCallback.java
similarity index 58%
copy from features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessor.java
copy to features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolverCallback.java
index 7a74fae..25340bc 100644
--- 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/region/SubsystemResolverCallback.java
@@ -16,27 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.karaf.features.internal.service;
+package org.apache.karaf.features.internal.region;
 
-import org.apache.karaf.features.Repository;
-import org.apache.karaf.features.internal.model.Features;
+import org.apache.karaf.features.BundleInfo;
 
 /**
- * Service that can process (enhance, modify, trim, ...) a set of features read from {@link Repository}.
+ * Additional callback methods that may be invoked from {@link SubsystemResolver}
  */
-public interface FeaturesProcessor {
+public interface SubsystemResolverCallback {
 
     /**
-     * Checks whether given repository URI is <em>blacklisted</em>
-     * @param uri
-     * @return
+     * Notification about {@link BundleInfo bundle} being blacklisted
+     * @param bundleInfo
      */
-    boolean isRepositoryBlacklisted(String uri);
-
-    /**
-     * Processes original {@link Features JAXB model of features}
-     * @param features
-     */
-    void process(Features features);
+    void bundleBlacklisted(BundleInfo bundleInfo);
 
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolverResolution.java b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolverResolution.java
new file mode 100644
index 0000000..ec12086
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolverResolution.java
@@ -0,0 +1,82 @@
+/*
+ * 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.region;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.FeaturesService;
+import org.eclipse.equinox.region.Region;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.resource.Resource;
+import org.osgi.resource.Wire;
+import org.osgi.service.repository.Repository;
+
+/**
+ * Public API of {@link SubsystemResolver} - for the purpose of documentation and categorization to public and internal
+ * methods. This interface groups methods related to resolution of {@link Subsystem subsystems}.
+ */
+public interface SubsystemResolverResolution {
+
+    /**
+     * <p>Prepares the resolver by configuring {@link Subsystem} hierarchy</p>
+     * <p>The input is a mapping from {@link Region region names} to a set of logical requirements.<br/>
+     * The effect is:<ul>
+     *     <li>A tree of {@link Subsystem subsystems} where the root subsystem represents {@link FeaturesService#ROOT_REGION}
+     *      with regions like <code>root/app1</code> represented as child subsystems.</li>
+     *     <li>A subsystem is created for each feature requirement and added as child and requirement for given region's subsystem</li>
+     *     <li>Each subsystem for a feature has optional requirements for conditional features</li>
+     * </ul></p>
+     *
+     * @param allFeatures all currently available features partitioned by name
+     * @param requirements desired mapping from regions to logical requirements
+     * @param system mapping from regions to unmanaged {@link BundleRevision}s
+     * @throws Exception
+     */
+    void prepare(Map<String, List<Feature>> allFeatures,
+                 Map<String, Set<String>> requirements,
+                 Map<String, Set<BundleRevision>> system) throws Exception;
+
+    /**
+     * Before attempting {@link #resolve resolution}, we can collect features' prerequisites. If there are any,
+     * caller may decide to deploy another set of requirements <strong>before</strong> the initial ones.
+     * Prerequisites allow to install for example <code>wrap</code> feature before installing a feature with bundle
+     * using <code>wrap:</code> protocol.
+     * @return
+     */
+    Set<String> collectPrerequisites();
+
+    /**
+     *
+     * @param featureResolutionRange
+     * @param serviceRequirements how to handle requirements from {@link org.osgi.namespace.service.ServiceNamespace#SERVICE_NAMESPACE}
+     * namespace
+     * @param globalRepository
+     * @param outputFile
+     * @return
+     * @throws Exception
+     */
+    public Map<Resource, List<Wire>> resolve(String featureResolutionRange,
+                                             FeaturesService.ServiceRequirementsBehavior serviceRequirements,
+                                             final Repository globalRepository,
+                                             String outputFile) throws Exception;
+
+}
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolverResult.java b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolverResult.java
new file mode 100644
index 0000000..216a97c
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolverResult.java
@@ -0,0 +1,78 @@
+/*
+ * 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.region;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.karaf.features.BundleInfo;
+import org.apache.karaf.features.internal.download.StreamProvider;
+import org.eclipse.equinox.region.RegionDigraph;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.resource.Resource;
+import org.osgi.resource.Wire;
+import org.osgi.service.resolver.ResolveContext;
+
+/**
+ * Public API of {@link SubsystemResolver} - for the purpose of documentation and categorization to public and internal
+ * methods. This interface groups methods invoked after performing resolution of {@link Subsystem subsystems}.
+ */
+public interface SubsystemResolverResult {
+
+    /**
+     * Get a map between regions, bundle locations and actual {@link BundleInfo}
+     * @return
+     */
+    Map<String, Map<String, BundleInfo>> getBundleInfos();
+
+    /**
+     * Get map of all downloaded resources (location -&gt; provider)
+     * @return
+     */
+    Map<String, StreamProvider> getProviders();
+
+    /**
+     * Returns a result of {@link org.osgi.service.resolver.Resolver#resolve(ResolveContext)}
+     * @return
+     */
+    Map<Resource, List<Wire>> getWiring();
+
+    /**
+     * Return directed graph of {@link org.eclipse.equinox.region.Region regions} after resolution.
+     * @return
+     * @throws BundleException
+     * @throws InvalidSyntaxException
+     */
+    RegionDigraph getFlatDigraph() throws BundleException, InvalidSyntaxException;
+
+    /**
+     * Returns a mapping between regions and a set of bundle {@link Resource resources}
+     * @return
+     */
+    Map<String, Set<Resource>> getBundlesPerRegions();
+
+    /**
+     * Returns a mapping between regions and a set of feature {@link Resource resources}
+     * @return
+     */
+    Map<String, Set<Resource>> getFeaturesPerRegions();
+
+}
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/FeatureResource.java b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/FeatureResource.java
index e0017a8..821931c 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/FeatureResource.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/FeatureResource.java
@@ -34,6 +34,12 @@ import static org.apache.karaf.features.internal.resolver.ResourceUtils.TYPE_FEA
 import static org.apache.karaf.features.internal.resolver.ResourceUtils.addIdentityRequirement;
 
 /**
+ * <p>An OSGi {@link Resource} representing Karaf feature. It has requirements on all its non-dependency
+ * (<code>dependency="false"</code>) bundles.</p>
+ * <p>It'll also use arbitrary capabilities ({@code <feature>/<capability>}) and requirements
+ * ({@code <feature>/<requirement>}).</p>
+ * <p>Dependant features ({@code <feature>/<feature>}) without <code>dependency="true"</code> will also be added
+ * as <code>osgi.identity</code> requirements with <code>type=karaf.feature</code>.</p>
  */
 public final class FeatureResource extends ResourceImpl {
 
@@ -46,15 +52,30 @@ public final class FeatureResource extends ResourceImpl {
         this.feature = feature;
     }
 
+    /**
+     * Constructs a {@link Resource} for conditional of a feature
+     * @param feature
+     * @param conditional
+     * @param featureRange
+     * @param locToRes
+     * @return
+     * @throws BundleException
+     */
     public static FeatureResource build(Feature feature, Conditional conditional, String featureRange, Map<String, ? extends Resource> locToRes) throws BundleException {
         Feature fcond = conditional.asFeature();
         FeatureResource resource = build(fcond, featureRange, locToRes);
         for (String cond : conditional.getCondition()) {
             if (cond.startsWith("req:")) {
+                // <conditional>/<condition>req:xxx</condition>
+                // conditional feature will require all its bundles and will have all declared, generic
+                // requirements
                 cond = cond.substring("req:".length());
                 List<Requirement> reqs = ResourceBuilder.parseRequirement(resource, cond);
                 resource.addRequirements(reqs);
             } else {
+                // <conditional>/<condition>xxx</condition>
+                // conditional feature will require all its bundles and will require the features that are the
+                // conditions with "condition:=true" directive
                 org.apache.karaf.features.internal.model.Dependency dep = new org.apache.karaf.features.internal.model.Dependency();
                 String[] p = cond.split("/");
                 dep.setName(p[0]);
@@ -67,14 +88,23 @@ public final class FeatureResource extends ResourceImpl {
         org.apache.karaf.features.internal.model.Dependency dep = new org.apache.karaf.features.internal.model.Dependency();
         dep.setName(feature.getName());
         dep.setVersion(feature.getVersion());
+        // conditional feature will also require parent feature - also with "condition:=true" directive
         addDependency(resource, dep, featureRange, true);
         return resource;
     }
 
+    /**
+     * Constructs {@link Resource} for given non-conditional feature.
+     * @param feature
+     * @param featureRange
+     * @param locToRes
+     * @return
+     * @throws BundleException
+     */
     public static FeatureResource build(Feature feature, String featureRange, Map<String, ? extends Resource> locToRes) throws BundleException {
         FeatureResource resource = new FeatureResource(feature);
         for (BundleInfo info : feature.getBundles()) {
-            if (!info.isDependency()) {
+            if (!info.isDependency() && !info.isBlacklisted()) {
                 Resource res = locToRes.get(info.getLocation());
                 if (res == null) {
                     throw new IllegalStateException("Resource not found for url " + info.getLocation());
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResolverUtil.java b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResolverUtil.java
index 2c23201..ec0744b 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResolverUtil.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResolverUtil.java
@@ -46,6 +46,11 @@ public class ResolverUtil
         return null;
     }
 
+    /**
+     * Returns name of owning {@link org.apache.karaf.features.internal.region.Subsystem} for given resource
+     * @param resource
+     * @return
+     */
     public static String getOwnerName(Resource resource)
     {
         List<Requirement> reqs = resource.getRequirements(null);
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceUtils.java b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceUtils.java
index c1798e9..d1c224c 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceUtils.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceUtils.java
@@ -69,6 +69,11 @@ public final class ResourceUtils {
         return null;
     }
 
+    /**
+     * If the resource has <code>type=karaf.feature</code> capability, returns its ID (name[/version]).
+     * @param resource
+     * @return
+     */
     public static String getFeatureId(Resource resource) {
         List<Capability> caps = resource.getCapabilities(null);
         for (Capability cap : caps) {
@@ -144,6 +149,12 @@ public final class ResourceUtils {
         }
     }
 
+    /**
+     * <p>Changes feature identifier (<code>name[/version]</code>) into a requirement specification.</p>
+     * <p>The OSGi manifest header for a feature will be: <code>osgi.identity;osgi.identity=feature-name;type=karaf.feature[;version=feature-version];filter:=filter-from-attrs</code></p>
+     * @param feature
+     * @return
+     */
     public static String toFeatureRequirement(String feature) {
         String[] parts = feature.split("/");
         Map<String, Object> attrs = new StringArrayMap<>(parts.length > 1 ? 3 : 2);
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/BundleInstallSupport.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/BundleInstallSupport.java
index 5865592..fb31a7a 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/BundleInstallSupport.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/BundleInstallSupport.java
@@ -33,6 +33,11 @@ import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.resource.Resource;
 import org.osgi.resource.Wire;
 
+/**
+ * <p>Interface to interact with OSGi framework.</p>
+ * <p>Bundles are installed into {@link org.eclipse.equinox.region.Region regions} and {@link Feature features}
+ * are used only to get their configs and libraries.</p>
+ */
 public interface BundleInstallSupport {
 
     void print(String message, boolean verbose);
@@ -71,14 +76,18 @@ public interface BundleInstallSupport {
     FrameworkInfo getInfo();
 
     void unregister();
-    
+
+    /**
+     * <p>Low-level state of system, provides information about start levels (initial and current), system bundle,
+     * bundle of features service and entire map of bundle IDs to {@link Bundle} instances.</p>
+     * <p>There's no relation to {@link org.eclipse.equinox.region.Region regions}.</p>
+     */
     class FrameworkInfo {
         public Bundle ourBundle;
         public Bundle systemBundle;
         public int initialBundleStartLevel;
         public int currentStartLevel;
         public Map<Long, Bundle> bundles = new HashMap<>();
-
     }
 
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/BundleInstallSupportImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/BundleInstallSupportImpl.java
index 232c57b..24c2fbf 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/BundleInstallSupportImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/BundleInstallSupportImpl.java
@@ -59,6 +59,10 @@ import org.osgi.resource.Wire;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+/**
+ * Interaction with OSGi framework, where bundles are installed into it via {@link RegionDigraph}. After a bundle
+ * is installed, it may be controlled in standard way via {@link Bundle} interface.
+ */
 public class BundleInstallSupportImpl implements BundleInstallSupport {
     private static final Logger LOGGER = LoggerFactory.getLogger(BundleInstallSupportImpl.class);
     
@@ -311,7 +315,7 @@ public class BundleInstallSupportImpl implements BundleInstallSupport {
         for (Bundle bundle : systemBundleContext.getBundles()) {
             info.bundles.put(bundle.getBundleId(), bundle);
         }
-        info.systemBundle = info.bundles.get(0);
+        info.systemBundle = info.bundles.get(0L);
         return info;
     }
 }
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 571e923..3c166d3 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
@@ -50,6 +50,7 @@ import org.apache.karaf.features.FeaturesService;
 import org.apache.karaf.features.internal.download.DownloadManager;
 import org.apache.karaf.features.internal.download.StreamProvider;
 import org.apache.karaf.features.internal.region.SubsystemResolver;
+import org.apache.karaf.features.internal.region.SubsystemResolverCallback;
 import org.apache.karaf.features.internal.resolver.FeatureResource;
 import org.apache.karaf.features.internal.resolver.ResolverUtil;
 import org.apache.karaf.features.internal.resolver.ResourceUtils;
@@ -83,8 +84,6 @@ import org.slf4j.LoggerFactory;
 
 import static org.apache.karaf.features.FeaturesService.ROOT_REGION;
 import static org.apache.karaf.features.FeaturesService.UPDATEABLE_URIS;
-import static org.apache.karaf.features.FeaturesService.UPDATE_SNAPSHOTS_ALWAYS;
-import static org.apache.karaf.features.FeaturesService.UPDATE_SNAPSHOTS_CRC;
 import static org.apache.karaf.features.internal.resolver.ResolverUtil.getSymbolicName;
 import static org.apache.karaf.features.internal.resolver.ResolverUtil.getVersion;
 import static org.apache.karaf.features.internal.resolver.ResourceUtils.TYPE_SUBSYSTEM;
@@ -111,7 +110,10 @@ import static org.osgi.framework.namespace.IdentityNamespace.TYPE_BUNDLE;
 
 public class Deployer {
 
-    public interface DeployCallback {
+    /**
+     * Interface through which {@link Deployer} interacts with OSGi framework.
+     */
+    public interface DeployCallback extends SubsystemResolverCallback {
         void print(String message, boolean verbose);
         void saveState(State state);
         void persistResolveRequest(DeploymentRequest request) throws IOException;
@@ -161,11 +163,17 @@ public class Deployer {
     }
 
     /**
-     * <p>Representation of the state of system from the point of view of <em>bundles</em> and <em>features</em></p>
+     * <p>Representation of the state of system from the point of view of <em>installed bundles</em>
+     * and <em>available features</em></p>
      */
     public static class DeploymentState {
-        /** Current {@link State} of system */
+        // part of the deployment state related to features service
+
+        /** Current {@link State} of features service */
         public State state;
+
+        // part of the deployment state related to low level OSGi framework (bundles, no regions)
+
         /** A {@link Bundle} providing {@link FeaturesService} */
         public Bundle serviceBundle;
         /** {@link org.osgi.framework.startlevel.FrameworkStartLevel#getInitialBundleStartLevel()} */
@@ -174,39 +182,97 @@ public class Deployer {
         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 */
+
+        // part of the deployment state related to all available features
+
+        /** feature-name -&gt; list of features for different versions for all available features (not only installed) */
+        private Map<String, List<Feature>> features;
+        /** feature-id -&gt; feature (not only installed) */
+        private Map<String, Feature> featuresById;
+
+        // part of the deployment state related to regions
+
+        /** region-name -&gt; ids for bundles installed in region (see {@link State#managedBundles}) */
         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;
+
+        /**
+         * Returns all features indexed by their name. For each name we have collection of {@link Feature features}
+         * for different versions.
+         * @return
+         */
+        public Map<String, List<Feature>> featuresByName() {
+            return features;
+        }
+
+        /**
+         * Returns all features indexed by their id.
+         * @return
+         */
+        public Map<String, Feature> featuresById() {
+            return featuresById;
+        }
+
+        /**
+         * Sets a list of features and stores it as map of features where the key is <code>name</code> and value is a
+         * list of features with different versions.
+         * @param featuresList
+         */
+        public void partitionFeatures(Collection<Feature> featuresList) {
+            features = new HashMap<>();
+            featuresById = new HashMap<>();
+            for (Feature feature : featuresList) {
+                features.computeIfAbsent(feature.getName(), name -> new ArrayList<>()).add(feature);
+                featuresById.put(feature.getId(), feature);
+            }
+        }
     }
 
     /**
-     * <p>A request to change current {@link State state} of system</p>
+     * <p>A request to change current {@link DeploymentState} 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;
+        /** A bnd macro that changes feature version into a version range. */
         public String featureResolutionRange;
-        public String serviceRequirements;
+        /** Indication of how to handle requirements from <code>osgi.service</code> namespace */
+        public FeaturesService.ServiceRequirementsBehavior serviceRequirements;
+        /** A bnd macro to find update'able version range for bundle versions (e.g., to determine whether to install or update a bundle */
         public String bundleUpdateRange;
-        public String updateSnaphots;
+        /** Indication of when to update bundles (or leave them as they are currently installed) */
+        public FeaturesService.SnapshotUpdateBehavior updateSnaphots;
+
+        /**
+         * Additional {@link Repository} that'll be used to resolve unresolved, non-optional requirements if
+         * they're not resolved against current
+         */
         public Repository globalRepository;
 
+        /** Target/desired set of requirements per region */
         public Map<String, Set<String>> requirements;
+        /** Target/desired set of features state per region */
         public Map<String, Map<String, FeatureState>> stateChanges;
+        /** Deployment options */
         public EnumSet<FeaturesService.Option> options;
+
+        /** File to store result of deployment */
         public String outputFile;
 
+        /**
+         * Prepare standard, empty DeploymentRequest, where feature versions are taken literally (no ranges)
+         * and bundle updates use <em>natural</em> range to determine between install and update (update on micro
+         * digit in version, e.g., <code>2.1.0</code> -&gt; <code>2.1.2</code>, but not <code>2.1.2</code> -&gt;
+         * <code>2.2.0</code>).
+         * @return
+         */
         public static DeploymentRequest defaultDeploymentRequest() {
             DeploymentRequest request = new DeploymentRequest();
             request.bundleUpdateRange = FeaturesService.DEFAULT_BUNDLE_UPDATE_RANGE;
             request.featureResolutionRange = FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE;
-            request.serviceRequirements = FeaturesService.SERVICE_REQUIREMENTS_DEFAULT;
-            request.overrides = new HashSet<>();
+            request.serviceRequirements = FeaturesService.ServiceRequirementsBehavior.Default;
             request.requirements = new HashMap<>();
             request.stateChanges = new HashMap<>();
             request.options = EnumSet.noneOf(FeaturesService.Option.class);
@@ -214,15 +280,24 @@ public class Deployer {
         }
     }
 
+    /**
+     * Deployment information for all regions
+     */
     static class Deployment {
         Map<Long, Long> bundleChecksums = new HashMap<>();
         Map<Resource, Bundle> resToBnd = new HashMap<>();
         Map<String, RegionDeployment> regions = new HashMap<>();
     }
 
+    /**
+     * Deployment information for single region
+     */
     static class RegionDeployment {
+        /** new {@link Resource resources} to install */
         List<Resource> toInstall = new ArrayList<>();
+        /** existing {@link Bundle bundles} to remove */
         List<Bundle> toDelete = new ArrayList<>();
+        /** existing {@link Bundle bundles} to update using new {@link Resource resources} */
         Map<Bundle, Resource> toUpdate = new HashMap<>();
     }
 
@@ -239,6 +314,29 @@ public class Deployer {
     }
 
     /**
+     * Performs full deployment - with prerequisites
+     *
+     * @param dstate  deployment state
+     * @param request deployment request
+     * @throws Exception in case of deployment failure.
+     */
+    public void deployFully(DeploymentState dstate, DeploymentRequest request) throws Exception {
+        Set<String> prereqs = new HashSet<>();
+        while (true) {
+            try {
+                deploy(dstate, request);
+                break;
+            } catch (Deployer.PartialDeploymentException e) {
+                if (!prereqs.containsAll(e.getMissing())) {
+                    prereqs.addAll(e.getMissing());
+                } else {
+                    throw new Exception("Deployment aborted due to loop in missing prerequisites: " + e.getMissing());
+                }
+            }
+        }
+    }
+
+    /**
      * Perform a deployment.
      *
      * @param dstate  deployment state
@@ -261,67 +359,30 @@ public class Deployer {
 
         // TODO: add an option to unmanage bundles instead of uninstalling those
 
+        // current managed bundles per region, as known by o.a.k.features.internal.service.FeaturesServiceImpl.state
         Map<String, Set<Long>> managedBundles = copy(dstate.state.managedBundles);
 
-        Map<String, Set<Bundle>> unmanagedBundles = apply(diff(dstate.bundlesPerRegion, dstate.state.managedBundles),
-                map(dstate.bundles));
+        // current not managed (by FeaturesService state) bundles per region, as known by o.a.k.features.internal.service.BundleInstallSupportImpl.digraph
+        // "unmanaged" means "not installed via features service"
+        Map<String, Set<Long>> diff = diff(dstate.bundlesPerRegion, dstate.state.managedBundles);
+        Map<String, Set<Bundle>> unmanagedBundles = apply(diff, map(dstate.bundles));
 
-        // Resolve
+        // Use Subsystem and Felix resolver
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, manager);
-        resolver.prepare(
-                dstate.features.values(),
-                request.requirements,
-                apply(unmanagedBundles, adapt(BundleRevision.class))
-        );
-        Set<String> prereqs = resolver.collectPrerequisites();
-        if (!prereqs.isEmpty()) {
-            for (Iterator<String> iterator = prereqs.iterator(); iterator.hasNext(); ) {
-                String prereq = iterator.next();
-                String[] parts = prereq.split("/");
-                String name = parts[0];
-                String version = parts[1];
-                VersionRange range = getRange(version, request.featureResolutionRange);
-                boolean found = false;
-                for (Set<String> featureSet : dstate.state.installedFeatures.values()) {
-                    for (String feature : featureSet) {
-                        String[] p = feature.split("/");
-                        found = name.equals(p[0]) && range.contains(VersionTable.getVersion(p[1]));
-                        if (found) {
-                            break;
-                        }
-                    }
-                    if (found) {
-                        break;
-                    }
-                }
-                if (found) {
-                    iterator.remove();
-                }
-            }
-        }
-        if (!prereqs.isEmpty()) {
-            if (request.requirements.get(ROOT_REGION).containsAll(prereqs)) {
-                throw new CircularPrerequisiteException(prereqs);
-            }
-            DeploymentRequest newRequest = new DeploymentRequest();
-            newRequest.bundleUpdateRange = request.bundleUpdateRange;
-            newRequest.featureResolutionRange = request.featureResolutionRange;
-            newRequest.serviceRequirements = request.serviceRequirements;
-            newRequest.globalRepository = request.globalRepository;
-            newRequest.options = request.options;
-            newRequest.overrides = request.overrides;
-            newRequest.requirements = copy(dstate.state.requirements);
-            for (String prereq : prereqs) {
-                addToMapSet(newRequest.requirements, ROOT_REGION, prereq);
-            }
-            newRequest.stateChanges = Collections.emptyMap();
-            newRequest.updateSnaphots = request.updateSnaphots;
-            deploy(dstate, newRequest);
-            throw new PartialDeploymentException(prereqs);
-        }
+        resolver.setDeployCallback(callback);
+        Map<String, Set<BundleRevision>> unmanagedBundleRevisions = apply(unmanagedBundles, adapt(BundleRevision.class));
+
+        // preparation - creating OSGi resources with reqs and caps for regions and features
+        resolver.prepare(dstate.featuresByName(), request.requirements, unmanagedBundleRevisions);
 
+        // if some features have prerequisites, we have to deploy them first - this method may throw Exception
+        // to start another cycle of deployment
+        handlePrerequisites(dstate, request, resolver);
+
+        // when there are no more prerequisites, we can resolve Subsystems and Features using Felix resolver
+        // Subsystem resolver will have then full information about new bundles and bundle updates or removals
+        // per region
         resolver.resolve(
-                request.overrides,
                 request.featureResolutionRange,
                 request.serviceRequirements,
                 request.globalRepository,
@@ -330,6 +391,7 @@ public class Deployer {
         Map<String, StreamProvider> providers = resolver.getProviders();
         Map<String, Set<Resource>> featuresPerRegion = resolver.getFeaturesPerRegions();
         Map<String, Set<String>> installedFeatures = apply(featuresPerRegion, featureId());
+        // changes to current state - added and removed features
         Map<String, Set<String>> newFeatures = diff(installedFeatures, dstate.state.installedFeatures);
         Map<String, Set<String>> delFeatures = diff(dstate.state.installedFeatures, installedFeatures);
 
@@ -365,7 +427,7 @@ public class Deployer {
             }
         }
 
-        // Compute information for each bundle
+        // Compute information for each bundle (region -> location -> BundleInfo)
         Map<String, Map<String, BundleInfo>> bundleInfos = resolver.getBundleInfos();
 
         //
@@ -800,7 +862,7 @@ public class Deployer {
                     addToMapSet(managedBundles, name, bundle.getBundleId());
                     deployment.resToBnd.put(resource, bundle);
                     // save a checksum of installed snapshot bundle
-                    if (UPDATE_SNAPSHOTS_CRC.equals(request.updateSnaphots)
+                    if (FeaturesService.SnapshotUpdateBehavior.Crc == request.updateSnaphots
                             && isUpdateable(resource) && !deployment.bundleChecksums.containsKey(bundle.getBundleId())) {
                         deployment.bundleChecksums.put(bundle.getBundleId(), crc);
                     }
@@ -847,7 +909,7 @@ public class Deployer {
         //
         if (!newFeatures.isEmpty()) {
             Set<String> featureIds = flatten(newFeatures);
-            for (Feature feature : dstate.features.values()) {
+            for (Feature feature : dstate.featuresById.values()) {
                 if (featureIds.contains(feature.getId())) {
                     callback.installConfigs(feature);
                     callback.installLibraries(feature);
@@ -935,7 +997,7 @@ public class Deployer {
         // Call listeners
         for (Map.Entry<String, Set<String>> entry : delFeatures.entrySet()) {
             for (String name : entry.getValue()) {
-                Feature feature = dstate.features.get(name);
+                Feature feature = dstate.featuresById.get(name);
                 if (feature != null) {
                     callback.callListeners(new FeatureEvent(FeatureEvent.EventType.FeatureUninstalled, feature, entry.getKey(), false));
                 }
@@ -943,7 +1005,7 @@ public class Deployer {
         }
         for (Map.Entry<String, Set<String>> entry : newFeatures.entrySet()) {
             for (String name : entry.getValue()) {
-                Feature feature = dstate.features.get(name);
+                Feature feature = dstate.featuresById.get(name);
                 if (feature != null) {
                     callback.callListeners(new FeatureEvent(FeatureEvent.EventType.FeatureInstalled, feature, entry.getKey(), false));
                 }
@@ -954,6 +1016,55 @@ public class Deployer {
         print("Done.", verbose);
     }
 
+    private void handlePrerequisites(DeploymentState dstate, DeploymentRequest request, SubsystemResolver resolver)
+            throws Exception {
+        Set<String> prereqs = resolver.collectPrerequisites();
+        if (!prereqs.isEmpty()) {
+            for (Iterator<String> iterator = prereqs.iterator(); iterator.hasNext(); ) {
+                String prereq = iterator.next();
+                String[] parts = prereq.split("/");
+                String name = parts[0];
+                String version = parts[1];
+                VersionRange range = getRange(version, request.featureResolutionRange);
+                boolean found = false;
+                for (Set<String> featureSet : dstate.state.installedFeatures.values()) {
+                    for (String feature : featureSet) {
+                        String[] p = feature.split("/");
+                        found = name.equals(p[0]) && range.contains(VersionTable.getVersion(p[1]));
+                        if (found) {
+                            break;
+                        }
+                    }
+                    if (found) {
+                        break;
+                    }
+                }
+                if (found) {
+                    iterator.remove();
+                }
+            }
+        }
+        if (!prereqs.isEmpty()) {
+            if (request.requirements.get(ROOT_REGION).containsAll(prereqs)) {
+                throw new CircularPrerequisiteException(prereqs);
+            }
+            DeploymentRequest newRequest = new DeploymentRequest();
+            newRequest.bundleUpdateRange = request.bundleUpdateRange;
+            newRequest.featureResolutionRange = request.featureResolutionRange;
+            newRequest.serviceRequirements = request.serviceRequirements;
+            newRequest.globalRepository = request.globalRepository;
+            newRequest.options = request.options;
+            newRequest.requirements = copy(dstate.state.requirements);
+            for (String prereq : prereqs) {
+                addToMapSet(newRequest.requirements, ROOT_REGION, prereq);
+            }
+            newRequest.stateChanges = Collections.emptyMap();
+            newRequest.updateSnaphots = request.updateSnaphots;
+            deploy(dstate, newRequest);
+            throw new PartialDeploymentException(prereqs);
+        }
+    }
+
     private static VersionRange getRange(String version, String featureResolutionRange) {
         VersionRange range;
         if (version.equals("0.0.0")) {
@@ -1251,10 +1362,10 @@ public class Deployer {
                         // and flag it as to update
                         if (isUpdateable(resource)) {
                             // Always update snapshots
-                            if (UPDATE_SNAPSHOTS_ALWAYS.equalsIgnoreCase(request.updateSnaphots)) {
+                            if (FeaturesService.SnapshotUpdateBehavior.Always == request.updateSnaphots) {
                                 LOGGER.debug("Update snapshot for " + bundle.getLocation());
                                 deployment.toUpdate.put(bundle, resource);
-                            } else if (UPDATE_SNAPSHOTS_CRC.equalsIgnoreCase(request.updateSnaphots)) {
+                            } else if (FeaturesService.SnapshotUpdateBehavior.Crc == request.updateSnaphots) {
                                 // Retrieve current bundle checksum
                                 long oldCrc;
                                 if (dstate.state.bundleChecksums.containsKey(bundleId)) {
@@ -1280,7 +1391,7 @@ public class Deployer {
                                             result.bundleChecksums.put(bundleId, oldCrc);
                                         }
                                     } catch (Throwable t) {
-                                        LOGGER.debug("Error calculating checksum for bundle: %s", bundle, t);
+                                        LOGGER.debug("Error calculating checksum for bundle: {}", bundle, t);
                                     }
                                 }
                                 // Compute new bundle checksum
@@ -1354,7 +1465,7 @@ public class Deployer {
 
     protected boolean isUpdateable(Resource resource) {
         String uri = getUri(resource);
-        return uri.matches(UPDATEABLE_URIS);
+        return uri != null && uri.matches(UPDATEABLE_URIS);
     }
 
     protected List<Bundle> getBundlesToStart(Collection<Bundle> bundles, Bundle serviceBundle) {
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
index 7a74fae..e8983d9 100644
--- 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
@@ -34,6 +34,13 @@ public interface FeaturesProcessor {
     boolean isRepositoryBlacklisted(String uri);
 
     /**
+     * Checks whether given bundle URI is <em>blacklisted</em>
+     * @param uri
+     * @return
+     */
+    boolean isBundleBlacklisted(String uri);
+
+    /**
      * Processes original {@link Features JAXB model of features}
      * @param 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
index c6ed5a6..4e3fa04 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessorImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessorImpl.java
@@ -50,6 +50,8 @@ public class FeaturesProcessorImpl implements FeaturesProcessor {
     public static Logger LOG = LoggerFactory.getLogger(FeaturesProcessorImpl.class);
 
     private static FeaturesProcessingSerializer serializer = new FeaturesProcessingSerializer();
+
+    // empty, but fully functional features processing configuration
     private FeaturesProcessing processing = new FeaturesProcessing();
 
     /**
@@ -66,7 +68,7 @@ public class FeaturesProcessorImpl implements FeaturesProcessor {
                     processing = serializer.read(stream);
                 }
             } catch (FileNotFoundException e) {
-                LOG.warn("Can't find feature processing file (" + featureModificationsURI + ")");
+                LOG.debug("Can't find feature processing file (" + featureModificationsURI + "), skipping");
             } catch (Exception e) {
                 LOG.warn("Can't initialize feature processor: " + e.getMessage());
             }
@@ -119,11 +121,12 @@ public class FeaturesProcessorImpl implements FeaturesProcessor {
     public void process(Features features) {
         // blacklisting features
         for (Feature feature : features.getFeature()) {
-            feature.setBlacklisted(isFeatureBlacklisted(feature));
+            boolean allBlacklisted = features.isBlacklisted();
+            feature.setBlacklisted(allBlacklisted || isFeatureBlacklisted(feature));
             // blacklisting bundles
-            processBundles(feature.getBundle());
+            processBundles(feature.getBundle(), allBlacklisted);
             for (Conditional c : feature.getConditional()) {
-                processBundles(c.getBundle());
+                processBundles(c.getBundle(), allBlacklisted);
             }
         }
 
@@ -132,9 +135,9 @@ public class FeaturesProcessorImpl implements FeaturesProcessor {
         // TODO: overriding features
     }
 
-    private void processBundles(List<Bundle> bundles) {
+    private void processBundles(List<Bundle> bundles, boolean allBlacklisted) {
         for (Bundle bundle : bundles) {
-            boolean bundleBlacklisted = isBundleBlacklisted(bundle.getLocation());
+            boolean bundleBlacklisted = allBlacklisted || isBundleBlacklisted(bundle.getLocation());
             if (bundleBlacklisted) {
                 // blacklisting has higher priority
                 bundle.setBlacklisted(true);
@@ -151,18 +154,23 @@ public class FeaturesProcessorImpl implements FeaturesProcessor {
      * @param bundle
      */
     private void staticOverrideBundle(Bundle bundle) {
+        bundle.setOverriden(BundleInfo.BundleOverrideMode.NONE);
+
         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);
+                if (override.getMode() == BundleReplacements.BundleOverrideMode.MAVEN) {
+                    bundle.setOverriden(BundleInfo.BundleOverrideMode.MAVEN);
+                } else {
+                    bundle.setOverriden(BundleInfo.BundleOverrideMode.OSGI);
+                }
                 bundle.setLocation(override.getReplacement());
-                // last rule wins - no break!!!
+                // TOCHECK: last rule wins - no break!!!
                 //break;
             }
         }
-
     }
 
     @Override
@@ -190,7 +198,8 @@ public class FeaturesProcessorImpl implements FeaturesProcessor {
      * @param location
      * @return
      */
-    private boolean isBundleBlacklisted(String location) {
+    @Override
+    public 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 1f1fdfd..7fb0f26 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
@@ -53,18 +53,50 @@ public class FeaturesServiceConfig {
      */
     public final String serviceRequirements;
 
+    /**
+     * Location of <code>etc/blacklisted.properties</code>
+     */
+    @Deprecated
     public final String blacklisted;
+
+    /**
+     * Location of <code>etc/org.apache.karaf.features.xml</code>
+     */
     public final String featureModifications;
+
+    /**
+     * Location of <code>etc/overrides.properties</code>
+     */
+    @Deprecated
     public final String overrides;
 
     public FeaturesServiceConfig() {
         this(null, null, null);
     }
 
+    public FeaturesServiceConfig(String featureModifications) {
+        this(null, FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE, FeaturesService.DEFAULT_BUNDLE_UPDATE_RANGE, null, 1, 0, 0, null, featureModifications, null);
+    }
+
+    @Deprecated
     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 featureResolutionRange, String bundleUpdateRange, String updateSnapshots, int downloadThreads, long scheduleDelay, int scheduleMaxRun, String featureModifications, String serviceRequirements) {
+        this.overrides = null;
+        this.featureResolutionRange = featureResolutionRange;
+        this.bundleUpdateRange = bundleUpdateRange;
+        this.updateSnapshots = updateSnapshots;
+        this.downloadThreads = downloadThreads;
+        this.scheduleDelay = scheduleDelay;
+        this.scheduleMaxRun = scheduleMaxRun;
+        this.blacklisted = null;
+        this.featureModifications = featureModifications;
+        this.serviceRequirements = serviceRequirements;
+    }
+
+    @Deprecated
     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;
@@ -77,4 +109,5 @@ public class FeaturesServiceConfig {
         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 5a6d788..0302478 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
@@ -51,6 +51,7 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.apache.felix.utils.version.VersionCleaner;
+import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.DeploymentEvent;
 import org.apache.karaf.features.DeploymentListener;
 import org.apache.karaf.features.Feature;
@@ -125,6 +126,9 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
 
     // Synchronized on lock
     private final Object lock = new Object();
+    /**
+     * {@link State} persisted to data directory of features.core bundle.
+     */
     private final State state = new State();
 
     private final ExecutorService executor;
@@ -223,7 +227,7 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
                 // Make sure we don't store bundle checksums if
                 // it has been disabled through configadmin
                 // so that we don't keep out-of-date checksums.
-                if (!UPDATE_SNAPSHOTS_CRC.equalsIgnoreCase(cfg.updateSnapshots)) {
+                if (!SnapshotUpdateBehavior.Crc.getValue().equalsIgnoreCase(cfg.updateSnapshots)) {
                     state.bundleChecksums.clear();
                 }
                 storage.save(state);
@@ -965,7 +969,7 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
         dstate.currentStartLevel = info.currentStartLevel;
         dstate.bundles = info.bundles;
         // Features
-        dstate.features = featuresById;
+        dstate.partitionFeatures(featuresById.values());
         RegionDigraph regionDigraph = installSupport.getDiGraphCopy();
         dstate.bundlesPerRegion = DigraphHelper.getBundlesPerRegion(regionDigraph);
         dstate.filtersPerRegion = DigraphHelper.getPolicies(regionDigraph);
@@ -973,13 +977,12 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
     }
 
     private Deployer.DeploymentRequest getDeploymentRequest(Map<String, Set<String>> requirements, Map<String, Map<String, FeatureState>> stateChanges, EnumSet<Option> options, String outputFile) {
-        Deployer.DeploymentRequest request = new Deployer.DeploymentRequest();
+        Deployer.DeploymentRequest request = Deployer.DeploymentRequest.defaultDeploymentRequest();
         request.bundleUpdateRange = cfg.bundleUpdateRange;
         request.featureResolutionRange = cfg.featureResolutionRange;
-        request.serviceRequirements = cfg.serviceRequirements;
-        request.updateSnaphots = cfg.updateSnapshots;
+        request.serviceRequirements = ServiceRequirementsBehavior.fromString(cfg.serviceRequirements);
+        request.updateSnaphots = SnapshotUpdateBehavior.fromString(cfg.updateSnapshots);
         request.globalRepository = globalRepository;
-        request.overrides = Overrides.loadOverrides(cfg.overrides);
         request.requirements = requirements;
         request.stateChanges = stateChanges;
         request.options = options;
@@ -1120,6 +1123,11 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
         installSupport.installLibraries(feature);
     }
 
+    @Override
+    public void bundleBlacklisted(BundleInfo bundleInfo) {
+
+    }
+
     private String join(Collection<FeatureReq> reqs) {
         return reqs.stream().map(FeatureReq::toString).collect(Collectors.joining(","));
     }
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 7d6f3e2..3d2d9b7 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
@@ -70,7 +70,10 @@ public final class Overrides {
      * @param resources the list of resources to resolve
      * @param overrides list of bundle overrides
      * @param <T> the resource type.
+     *
+     * @deprecated Use {@link #override(Map, Map)}
      */
+    @Deprecated
     public static <T extends Resource> void override(Map<String, T> resources, Collection<String> overrides) {
         // Do override replacement
         for (Clause override : Parser.parseClauses(overrides.toArray(new String[overrides.size()]))) {
@@ -90,6 +93,35 @@ public final class Overrides {
     }
 
     /**
+     * <p>Input map of resources is checked - if there are matching resources in <code>overridenFrom</code> and
+     * there's <strong>no</strong> symbolic name matching, resource for original URI is restored.
+     * Effectively this method reverts {@link org.apache.karaf.features.internal.model.processing.BundleReplacements.BundleOverrideMode#MAVEN maven}
+     * override mode if there's no symbolic name matching.</p>
+     *
+     * <p>About versions - with previous <code>${karaf.etc}/overrides.properties</code> both symbolic name
+     * should match <strong>and</strong> versions should be compatible - either using implicit rules or by means
+     * of <code>range</code> clause. With new mechanism, we know we should use OSGi or Maven override, but we
+     * loose information about OSGi version range matching - we assume then that version rules were applied at
+     * features JAXB model processing time.</p>
+     *
+     * @param resources
+     * @param overridenFrom
+     * @param <T>
+     */
+    public static <T extends Resource> void override(Map<String, T> resources, Map<String, T> overridenFrom) {
+        for (Map.Entry<String, T> original : overridenFrom.entrySet()) {
+            T replacement = resources.get(original.getKey());
+            if (replacement == null) {
+                continue;
+            }
+            if (!shouldOverride(original.getValue(), replacement, "[0,*)")) {
+                // bring back original version
+                resources.put(original.getKey(), original.getValue());
+            }
+        }
+    }
+
+    /**
      * @param resource resource to be overriden
      * @param explicitRange range set on the override clause
      * @return if the resource should be overriden by the given override
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 3d76c23..c734e8a 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
@@ -89,6 +89,7 @@ public class RepositoryImpl implements Repository {
 
     public void setBlacklisted(boolean blacklisted) {
         this.blacklisted = blacklisted;
+        features.setBlacklisted(blacklisted);
     }
 
     private void load(boolean validate) {
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 b89947b..e67d4a7 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
@@ -25,32 +25,33 @@ 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>.
+ * <p>Representation of the state of features service from the point of view of <em>logical requirements</em>
+ * which are translated into bundles and features installed in {@link org.eclipse.equinox.region.Region regions}.
  * It's a collection of:<ul>
  *     <li>used repositories</li>
- *     <li>region -&gt; requirements</li>
- *     <li>region -&gt; installed features</li>
+ *     <li>region -&gt; requirements (logical feature requirements)</li>
+ *     <li>region -&gt; installed features (actual features installed - including conditionals and dependant features)</li>
  *     <li>region -&gt; installed features -&gt; state of feature installation</li>
- *     <li>region -&gt; bundle ids</li>
+ *     <li>region -&gt; bundle ids (for bundles installed via features service, a.k.a. <em>managed bundles</em>)</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>
+ * before resolving/refreshing them. Before State is set, work is done on the instance of Deployer.DeploymentState.</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 (<code>feature:name/version-range</code>) */
     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 (<code>name/version</code>) */
     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 (<code>name/version</code>) */
     public final Map<String, Map<String, String>> stateFeatures = new HashMap<>();
 
-    /** Map from region name to Set of installed bundle ids */
+    /** Map from region name to Set of ids of bundles installed via some features or requirements */
     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<>();
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/util/MapUtils.java b/features/core/src/main/java/org/apache/karaf/features/internal/util/MapUtils.java
index 92128f0..f3dd158 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/util/MapUtils.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/util/MapUtils.java
@@ -42,6 +42,18 @@ public final class MapUtils {
         return inverted;
     }
 
+    /**
+     * Changes mapping from <code>S</code> -&gt; <code>Set&lt;T&gt;</code> to mapping
+     * <code>S</code> -&gt; <code>Set&lt;U&gt;</code> using {@link Function} that can change <code>T</code> to
+     * <code>U</code>.
+     *
+     * @param mapset
+     * @param function
+     * @param <S> A key that maps to set of values in input and result map
+     * @param <T> A type of input set of values
+     * @param <U> A type of result set of values
+     * @return
+     */
     public static <S, T, U> Map<S, Set<U>> apply(Map<S, Set<T>> mapset, Function<T, U> function) {
         Map<S, Set<U>> result = new HashMap<>(mapset.size());
         for (Map.Entry<S, Set<T>> entry : mapset.entrySet()) {
@@ -94,6 +106,15 @@ public final class MapUtils {
         return set;
     }
 
+    /**
+     * Produces a map where each set value in <code>from</code> map has every element that's in <code>to</code>
+     * map's set value removed. If <code>from</code> map is left with empty set value, entire set is removed.
+     * @param from
+     * @param to
+     * @param <S>
+     * @param <T>
+     * @return
+     */
     public static <S, T> Map<S, Set<T>> diff(Map<S, Set<T>> from, Map<S, Set<T>> to) {
         Map<S, Set<T>> diff = copyMapSet(from);
         remove(diff, to);
@@ -118,6 +139,14 @@ public final class MapUtils {
         }
     }
 
+    /**
+     * Removes all values from <code>toRemove</code> map from <code>from</code> map. After removal, set values
+     * in <code>from</code> map may be smaller or removed entirely (if there are no more values in given set).
+     * @param from
+     * @param toRemove
+     * @param <S>
+     * @param <T>
+     */
     public static <S, T> void remove(Map<S, Set<T>> from, Map<S, Set<T>> toRemove) {
         for (Map.Entry<S, Set<T>> entry : toRemove.entrySet()) {
             Set<T> s = from.get(entry.getKey());
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/region/FeaturesDependenciesTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/region/FeaturesDependenciesTest.java
index c482534..036e27d 100644
--- a/features/core/src/test/java/org/apache/karaf/features/internal/region/FeaturesDependenciesTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/region/FeaturesDependenciesTest.java
@@ -26,8 +26,10 @@ import java.util.Map;
 import java.util.Set;
 
 import org.apache.felix.resolver.ResolverImpl;
+import org.apache.karaf.features.Feature;
 import org.apache.karaf.features.FeaturesService;
 import org.apache.karaf.features.internal.resolver.Slf4jResolverLog;
+import org.apache.karaf.features.internal.service.Deployer;
 import org.apache.karaf.features.internal.service.RepositoryImpl;
 import org.apache.karaf.features.internal.support.TestDownloadManager;
 import org.junit.Test;
@@ -141,11 +143,10 @@ public class FeaturesDependenciesTest {
         }
 
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data8"));
-        resolver.prepare(Arrays.asList(repo.getFeatures()),
+        resolver.prepare(partitionByName(repo.getFeatures()),
                 requirements,
                 Collections.emptyMap());
-        resolver.resolve(Collections.emptySet(),
-                FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                 null, null, null);
 
         verify(resolver, expected);
@@ -203,4 +204,10 @@ public class FeaturesDependenciesTest {
                 + cap.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE);
     }
 
+    private Map<String, List<Feature>> partitionByName(Feature[] features) {
+        Deployer.DeploymentState ds = new Deployer.DeploymentState();
+        ds.partitionFeatures(Arrays.asList(features));
+        return ds.featuresByName();
+    }
+
 }
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java
index f6b6563..8984aba 100644
--- a/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java
@@ -26,9 +26,12 @@ import java.util.Map;
 import java.util.Set;
 
 import org.apache.felix.resolver.ResolverImpl;
+import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.Feature;
 import org.apache.karaf.features.FeaturesService;
+import org.apache.karaf.features.internal.model.Bundle;
 import org.apache.karaf.features.internal.resolver.Slf4jResolverLog;
+import org.apache.karaf.features.internal.service.Deployer;
 import org.apache.karaf.features.internal.service.RepositoryImpl;
 import org.apache.karaf.features.internal.support.TestDownloadManager;
 import org.junit.Test;
@@ -62,11 +65,35 @@ public class SubsystemTest {
         addToMapSet(expected, "root/apps1", "b/1.0.0");
 
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data1"));
-        resolver.prepare(Arrays.asList(repo.getFeatures()),
+        resolver.prepare(partitionByName(repo.getFeatures()),
                          features,
                          Collections.emptyMap());
-        resolver.resolve(Collections.emptySet(),
-                         FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+                         null, null, null);
+
+        verify(resolver, expected);
+    }
+
+    @Test
+    public void test1a() throws Exception {
+        RepositoryImpl repo = new RepositoryImpl(getClass().getResource("data1/features.xml").toURI());
+
+        Map<String, Set<String>> features = new HashMap<>();
+        addToMapSet(features, "root", "f1");
+        addToMapSet(features, "root/apps1", "f2");
+        addToMapSet(features, "root/apps1/regionx", "bundle:d");
+
+        Map<String, Set<String>> expected = new HashMap<>();
+        addToMapSet(expected, "root", "a/1.0.0");
+        addToMapSet(expected, "root", "c/1.0.0");
+        addToMapSet(expected, "root/apps1", "b/1.0.0");
+        addToMapSet(expected, "root/apps1/regionx", "d/1.0.0");
+
+        SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data1"));
+        resolver.prepare(partitionByName(repo.getFeatures()),
+                         features,
+                         Collections.emptyMap());
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                          null, null, null);
 
         verify(resolver, expected);
@@ -93,20 +120,24 @@ public class SubsystemTest {
         addToMapSet(expected, "root/apps2#f1", "a/1.0.0");
 
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data2"));
-        resolver.prepare(Arrays.asList(repo.getFeatures()),
+        resolver.prepare(partitionByName(repo.getFeatures()),
                          features,
                          Collections.emptyMap());
-        resolver.resolve(Collections.emptySet(),
-                         FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                          null, null, null);
 
         verify(resolver, expected);
     }
 
     @Test
-    public void testOverrides() throws Exception {
+    public void testOverridesCompatibilityModeSymbolicNameMatches() throws Exception {
         RepositoryImpl repo = new RepositoryImpl(getClass().getResource("data3/features.xml").toURI());
 
+        // this is normally done by features processor
+        ((Bundle)repo.getFeatures()[0].getBundles().get(0)).setOverriden(BundleInfo.BundleOverrideMode.OSGI);
+        ((Bundle)repo.getFeatures()[0].getBundles().get(0)).setOriginalLocation("a");
+        ((Bundle)repo.getFeatures()[0].getBundles().get(0)).setLocation("b");
+
         Map<String, Set<String>> features = new HashMap<>();
         addToMapSet(features, "root/apps1", "f1");
 
@@ -114,11 +145,62 @@ public class SubsystemTest {
         addToMapSet(expected, "root/apps1", "a/1.0.1");
 
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data3"));
-        resolver.prepare(Arrays.asList(repo.getFeatures()),
+        resolver.prepare(partitionByName(repo.getFeatures()),
+                         features,
+                         Collections.emptyMap());
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+                         null, null, null);
+
+        verify(resolver, expected);
+    }
+
+    @Test
+    public void testOverridesCompatibilityModeSymbolicDoesNotMatch() throws Exception {
+        RepositoryImpl repo = new RepositoryImpl(getClass().getResource("data3/features.xml").toURI());
+
+        // this is normally done by features processor
+        ((Bundle)repo.getFeatures()[0].getBundles().get(0)).setOverriden(BundleInfo.BundleOverrideMode.OSGI);
+        ((Bundle)repo.getFeatures()[0].getBundles().get(0)).setOriginalLocation("a");
+        ((Bundle)repo.getFeatures()[0].getBundles().get(0)).setLocation("c");
+
+        Map<String, Set<String>> features = new HashMap<>();
+        addToMapSet(features, "root/apps1", "f1");
+
+        // we expect override not to be used - symbolic name between a and c doesn't match
+        Map<String, Set<String>> expected = new HashMap<>();
+        addToMapSet(expected, "root/apps1", "a/1.0.0");
+
+        SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data3"));
+        resolver.prepare(partitionByName(repo.getFeatures()),
+                         features,
+                         Collections.emptyMap());
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+                         null, null, null);
+
+        verify(resolver, expected);
+    }
+
+    @Test
+    public void testOverridesMavenMode() throws Exception {
+        RepositoryImpl repo = new RepositoryImpl(getClass().getResource("data3/features.xml").toURI());
+
+        // this is normally done by features processor
+        ((Bundle)repo.getFeatures()[0].getBundles().get(0)).setOverriden(BundleInfo.BundleOverrideMode.MAVEN);
+        ((Bundle)repo.getFeatures()[0].getBundles().get(0)).setOriginalLocation("a");
+        ((Bundle)repo.getFeatures()[0].getBundles().get(0)).setLocation("c");
+
+        Map<String, Set<String>> features = new HashMap<>();
+        addToMapSet(features, "root/apps1", "f1");
+
+        // we expect override to be used - symbolic name between a and c doesn't match, but we don't care
+        Map<String, Set<String>> expected = new HashMap<>();
+        addToMapSet(expected, "root/apps1", "not-a/1.0.1");
+
+        SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data3"));
+        resolver.prepare(partitionByName(repo.getFeatures()),
                          features,
                          Collections.emptyMap());
-        resolver.resolve(Collections.singleton("b"),
-                         FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                          null, null, null);
 
         verify(resolver, expected);
@@ -134,11 +216,10 @@ public class SubsystemTest {
         addToMapSet(expected, "root/apps1", "a/1.0.0");
 
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data4"));
-        resolver.prepare(Arrays.asList(repo.getFeatures()),
+        resolver.prepare(partitionByName(repo.getFeatures()),
                          features,
                          Collections.emptyMap());
-        resolver.resolve(Collections.emptySet(),
-                         FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                          null, null, null);
 
         verify(resolver, expected);
@@ -156,11 +237,10 @@ public class SubsystemTest {
         addToMapSet(expected, "root/apps1", "b/1.0.0");
 
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data4"));
-        resolver.prepare(Arrays.asList(repo.getFeatures()),
+        resolver.prepare(partitionByName(repo.getFeatures()),
                          features,
                          Collections.emptyMap());
-        resolver.resolve(Collections.emptySet(),
-                         FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                          null, null, null);
 
         verify(resolver, expected);
@@ -178,11 +258,10 @@ public class SubsystemTest {
         addToMapSet(expected, "root/apps1", "c/1.0.0");
 
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data1"));
-        resolver.prepare(Arrays.asList(repo.getFeatures()),
+        resolver.prepare(partitionByName(repo.getFeatures()),
                 features,
                 Collections.emptyMap());
-        resolver.resolve(Collections.emptySet(),
-                FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                 null, null, null);
 
         verify(resolver, expected);
@@ -199,11 +278,10 @@ public class SubsystemTest {
         addToMapSet(expected, "root", "b/1.0.0");
 
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data5"));
-        resolver.prepare(Arrays.asList(repo.getFeatures()),
+        resolver.prepare(partitionByName(repo.getFeatures()),
                 features,
                 Collections.emptyMap());
-        resolver.resolve(Collections.emptySet(),
-                FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                 null, null, null);
 
         verify(resolver, expected);
@@ -221,11 +299,10 @@ public class SubsystemTest {
         addToMapSet(expected, "root", "c/1.0.0");
 
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data5"));
-        resolver.prepare(Arrays.asList(repo.getFeatures()),
+        resolver.prepare(partitionByName(repo.getFeatures()),
                 features,
                 Collections.emptyMap());
-        resolver.resolve(Collections.emptySet(),
-                FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                 null, null, null);
 
         verify(resolver, expected);
@@ -243,11 +320,10 @@ public class SubsystemTest {
         addToMapSet(expected, "root", "c/1.0.0");
 
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data6"));
-        resolver.prepare(Arrays.asList(repo.getFeatures()),
+        resolver.prepare(partitionByName(repo.getFeatures()),
                 features,
                 Collections.emptyMap());
-        resolver.resolve(Collections.emptySet(),
-                FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                 null, null, null);
 
         verify(resolver, expected);
@@ -267,11 +343,10 @@ public class SubsystemTest {
         addToMapSet(expected, "root/apps1", "b/1.0.0");
 
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data7"));
-        resolver.prepare(Arrays.asList(repo.getFeatures()),
+        resolver.prepare(partitionByName(repo.getFeatures()),
                 features,
                 Collections.emptyMap());
-        resolver.resolve(Collections.emptySet(),
-                FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                 null, null, null);
 
         verify(resolver, expected);
@@ -294,12 +369,37 @@ public class SubsystemTest {
         addToMapSet(expected, "root", "pax-web-tomcat/6.0.4");
         addToMapSet(expected, "root", "pax-web-api/6.0.4");
 
+        Deployer.DeploymentState ds = new Deployer.DeploymentState();
+        ds.partitionFeatures(allFeatures);
         SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data9"));
-        resolver.prepare(allFeatures,
+        resolver.prepare(ds.featuresByName(),
                 features,
                 Collections.emptyMap());
-        resolver.resolve(Collections.emptySet(),
-                FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
+                null, null, null);
+
+        verify(resolver, expected);
+    }
+
+    @Test
+    public void testBlacklistedBundles() throws Exception {
+        RepositoryImpl repo = new RepositoryImpl(getClass().getResource("data10/features.xml").toURI());
+
+        // this is normally done by features processor
+        ((Bundle)repo.getFeatures()[0].getBundles().get(1)).setBlacklisted(true);
+
+        Map<String, Set<String>> features = new HashMap<>();
+        addToMapSet(features, "root", "f1");
+
+        Map<String, Set<String>> expected = new HashMap<>();
+        // we expect only bundle "a", as "b" is blacklisted
+        addToMapSet(expected, "root", "a/1.0.0");
+
+        SubsystemResolver resolver = new SubsystemResolver(this.resolver, new TestDownloadManager(getClass(), "data10"));
+        resolver.prepare(partitionByName(repo.getFeatures()),
+                features,
+                Collections.emptyMap());
+        resolver.resolve(FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                 null, null, null);
 
         verify(resolver, expected);
@@ -357,4 +457,10 @@ public class SubsystemTest {
                 + cap.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE);
     }
 
+    private Map<String, List<Feature>> partitionByName(Feature[] features) {
+        Deployer.DeploymentState ds = new Deployer.DeploymentState();
+        ds.partitionFeatures(Arrays.asList(features));
+        return ds.featuresByName();
+    }
+
 }
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/DeployerTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/DeployerTest.java
index a08d7c5..a124e8d 100644
--- a/features/core/src/test/java/org/apache/karaf/features/internal/service/DeployerTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/DeployerTest.java
@@ -19,6 +19,7 @@ package org.apache.karaf.features.internal.service;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -32,6 +33,7 @@ import java.util.jar.Manifest;
 
 import org.apache.felix.resolver.ResolverImpl;
 import org.apache.felix.utils.version.VersionRange;
+import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.DeploymentEvent;
 import org.apache.karaf.features.Feature;
 import org.apache.karaf.features.FeatureEvent;
@@ -113,9 +115,7 @@ public class DeployerTest {
         dstate.state = new State();
         dstate.bundles = new HashMap<>();
         dstate.bundlesPerRegion = new HashMap<>();
-        dstate.features = new HashMap<>();
-        dstate.features.put(f100.getId(), f100);
-        dstate.features.put(f101.getId(), f101);
+        dstate.partitionFeatures(Arrays.asList(f100, f101));
         dstate.filtersPerRegion = new HashMap<>();
         dstate.filtersPerRegion.put(ROOT_REGION, new HashMap<>());
 
@@ -124,9 +124,8 @@ public class DeployerTest {
         request.featureResolutionRange = DEFAULT_FEATURE_RESOLUTION_RANGE;
         request.globalRepository = null;
         request.options = EnumSet.noneOf(Option.class);
-        request.overrides = Collections.emptySet();
         request.stateChanges = Collections.emptyMap();
-        request.updateSnaphots = UPDATE_SNAPSHOTS_NONE;
+        request.updateSnaphots = SnapshotUpdateBehavior.None;
         request.requirements = new HashMap<>();
         addToMapSet(request.requirements, ROOT_REGION, f100.getName() + "/" + new VersionRange(f100.getVersion(), true));
 
@@ -215,9 +214,7 @@ public class DeployerTest {
         dstate.bundles.put(1L, bundleA);
         dstate.bundlesPerRegion = new HashMap<>();
         addToMapSet(dstate.bundlesPerRegion, ROOT_REGION, 1L);
-        dstate.features = new HashMap<>();
-        dstate.features.put(f100.getId(), f100);
-        dstate.features.put(f101.getId(), f101);
+        dstate.partitionFeatures(Arrays.asList(f100, f101));
         dstate.filtersPerRegion = new HashMap<>();
         dstate.filtersPerRegion.put(ROOT_REGION, new HashMap<>());
 
@@ -226,9 +223,8 @@ public class DeployerTest {
         request.featureResolutionRange = DEFAULT_FEATURE_RESOLUTION_RANGE;
         request.globalRepository = null;
         request.options = EnumSet.noneOf(Option.class);
-        request.overrides = Collections.emptySet();
         request.stateChanges = Collections.emptyMap();
-        request.updateSnaphots = UPDATE_SNAPSHOTS_NONE;
+        request.updateSnaphots = SnapshotUpdateBehavior.None;
         request.requirements = new HashMap<>();
         addToMapSet(request.requirements, ROOT_REGION, f101.getName() + "/" + new VersionRange(f101.getVersion(), true));
 
@@ -286,7 +282,7 @@ public class DeployerTest {
         dstate.bundles.put(serviceBundle.getBundleId(), serviceBundle);
         dstate.bundlesPerRegion = new HashMap<>();
         addToMapSet(dstate.bundlesPerRegion, ROOT_REGION, serviceBundle.getBundleId());
-        dstate.features = Collections.singletonMap(f1.getId(), f1);
+        dstate.partitionFeatures(Collections.singletonList(f1));
         dstate.filtersPerRegion = new HashMap<>();
         dstate.filtersPerRegion.put(ROOT_REGION, new HashMap<>());
 
@@ -295,9 +291,8 @@ public class DeployerTest {
         request.featureResolutionRange = DEFAULT_FEATURE_RESOLUTION_RANGE;
         request.globalRepository = null;
         request.options = EnumSet.noneOf(Option.class);
-        request.overrides = Collections.emptySet();
         request.stateChanges = Collections.emptyMap();
-        request.updateSnaphots = UPDATE_SNAPSHOTS_NONE;
+        request.updateSnaphots = SnapshotUpdateBehavior.None;
         request.requirements = new HashMap<>();
         addToMapSet(request.requirements, ROOT_REGION, f1.getName());
 
@@ -358,9 +353,7 @@ public class DeployerTest {
         dstate.state = new State();
         dstate.bundles = new HashMap<>();
         dstate.bundlesPerRegion = new HashMap<>();
-        dstate.features = new HashMap<>();
-        dstate.features.put(f1.getId(), f1);
-        dstate.features.put(f2.getId(), f2);
+        dstate.partitionFeatures(Arrays.asList(f1, f2));
         dstate.filtersPerRegion = new HashMap<>();
         dstate.filtersPerRegion.put(ROOT_REGION, new HashMap<>());
 
@@ -369,9 +362,8 @@ public class DeployerTest {
         request.featureResolutionRange = DEFAULT_FEATURE_RESOLUTION_RANGE;
         request.globalRepository = null;
         request.options = EnumSet.noneOf(Option.class);
-        request.overrides = Collections.emptySet();
         request.stateChanges = Collections.emptyMap();
-        request.updateSnaphots = UPDATE_SNAPSHOTS_NONE;
+        request.updateSnaphots = SnapshotUpdateBehavior.None;
         request.requirements = new HashMap<>();
         addToMapSet(request.requirements, ROOT_REGION, f2.getName());
 
@@ -425,9 +417,7 @@ public class DeployerTest {
         dstate.bundles.put(serviceBundle1.getBundleId(), serviceBundle1);
         dstate.bundlesPerRegion = new HashMap<>();
         addToMapSet(dstate.bundlesPerRegion, ROOT_REGION, serviceBundle1.getBundleId());
-        dstate.features = new HashMap<>();
-        dstate.features.put(f1.getId(), f1);
-        dstate.features.put(f2.getId(), f2);
+        dstate.partitionFeatures(Arrays.asList(f1, f2));
         dstate.filtersPerRegion = new HashMap<>();
         dstate.filtersPerRegion.put(ROOT_REGION, new HashMap<>());
 
@@ -436,9 +426,8 @@ public class DeployerTest {
         request.featureResolutionRange = DEFAULT_FEATURE_RESOLUTION_RANGE;
         request.globalRepository = null;
         request.options = EnumSet.noneOf(Option.class);
-        request.overrides = Collections.emptySet();
         request.stateChanges = Collections.emptyMap();
-        request.updateSnaphots = UPDATE_SNAPSHOTS_NONE;
+        request.updateSnaphots = SnapshotUpdateBehavior.None;
         request.requirements = new HashMap<>();
         addToMapSet(request.requirements, ROOT_REGION, f2.getName());
 
@@ -485,10 +474,7 @@ public class DeployerTest {
         dstate.state = new State();
         dstate.bundles = new HashMap<>();
         dstate.bundlesPerRegion = new HashMap<>();
-        dstate.features = new HashMap<>();
-        for (Feature f : repo.getFeatures()) {
-            dstate.features.put(f.getId(), f);
-        }
+        dstate.partitionFeatures(Arrays.asList(repo.getFeatures()));
         dstate.filtersPerRegion = new HashMap<>();
         dstate.filtersPerRegion.put(ROOT_REGION, new HashMap<>());
 
@@ -497,9 +483,8 @@ public class DeployerTest {
         request.featureResolutionRange = DEFAULT_FEATURE_RESOLUTION_RANGE;
         request.globalRepository = null;
         request.options = EnumSet.noneOf(Option.class);
-        request.overrides = Collections.emptySet();
         request.stateChanges = Collections.emptyMap();
-        request.updateSnaphots = UPDATE_SNAPSHOTS_NONE;
+        request.updateSnaphots = SnapshotUpdateBehavior.None;
 
         MyDeployCallback callback = new MyDeployCallback(dstate, bundles);
         Deployer deployer = new Deployer(manager, resolver, callback);
@@ -647,5 +632,10 @@ public class DeployerTest {
         public void installLibraries(Feature feature) throws IOException {
 
         }
+
+        @Override
+        public void bundleBlacklisted(BundleInfo bundleInfo) {
+
+        }
     }
 }
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
index 63cc100..e73e7d1 100644
--- 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
@@ -24,6 +24,7 @@ import javax.xml.bind.Marshaller;
 
 import org.apache.felix.utils.manifest.Clause;
 import org.apache.felix.utils.version.VersionRange;
+import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.Feature;
 import org.apache.karaf.features.internal.model.Bundle;
 import org.apache.karaf.features.internal.model.processing.BundleReplacements;
@@ -186,18 +187,18 @@ public class FeaturesProcessorTest {
         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());
+        assertTrue(f1.getBundles().get(0).isOverriden() == BundleInfo.BundleOverrideMode.NONE);
+        assertTrue(f1.getBundles().get(1).isOverriden() == BundleInfo.BundleOverrideMode.OSGI);
         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());
+        assertTrue(f1.getBundles().get(2).isOverriden() == BundleInfo.BundleOverrideMode.MAVEN);
         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());
+        assertTrue(f1.getBundles().get(3).isOverriden() == BundleInfo.BundleOverrideMode.NONE);
+        assertTrue(f1.getConditional().get(0).getBundles().get(0).isOverriden() == BundleInfo.BundleOverrideMode.OSGI);
         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());
+        assertTrue(f1.getConditional().get(0).getBundles().get(1).isOverriden() == BundleInfo.BundleOverrideMode.NONE);
     }
 
     @Test
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/region/data1/d.mf b/features/core/src/test/resources/org/apache/karaf/features/internal/region/data1/d.mf
new file mode 100644
index 0000000..2ef8fc0
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/region/data1/d.mf
@@ -0,0 +1,6 @@
+Manifest-Version: 1
+Bundle-ManifestVersion: 2
+Bundle-SymbolicName: d
+Bundle-Version: 1.0.0
+Require-Capability: ns;filter:="(ns=c)"
+
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/region/data10/a.mf b/features/core/src/test/resources/org/apache/karaf/features/internal/region/data10/a.mf
new file mode 100644
index 0000000..20a7811
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/region/data10/a.mf
@@ -0,0 +1,5 @@
+Manifest-Version: 1
+Bundle-ManifestVersion: 2
+Bundle-SymbolicName: a
+Bundle-Version: 1.0.0
+
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/region/data10/b.mf b/features/core/src/test/resources/org/apache/karaf/features/internal/region/data10/b.mf
new file mode 100644
index 0000000..dc96158
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/region/data10/b.mf
@@ -0,0 +1,5 @@
+Manifest-Version: 1
+Bundle-ManifestVersion: 2
+Bundle-SymbolicName: b
+Bundle-Version: 1.0.0
+
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/region/data10/features.xml b/features/core/src/test/resources/org/apache/karaf/features/internal/region/data10/features.xml
new file mode 100644
index 0000000..03c9853
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/region/data10/features.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="test" xmlns="http://karaf.apache.org/xmlns/features/v1.3.0">
+
+    <feature name="f1">
+        <bundle>a</bundle>
+        <bundle>b</bundle>
+    </feature>
+
+</features>
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/region/data3/c.mf b/features/core/src/test/resources/org/apache/karaf/features/internal/region/data3/c.mf
new file mode 100644
index 0000000..4fe2569
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/region/data3/c.mf
@@ -0,0 +1,5 @@
+Manifest-Version: 1
+Bundle-ManifestVersion: 2
+Bundle-SymbolicName: not-a
+Bundle-Version: 1.0.1
+
diff --git a/profile/pom.xml b/profile/pom.xml
index 118a14f..a3c9e11 100644
--- a/profile/pom.xml
+++ b/profile/pom.xml
@@ -177,18 +177,18 @@
                             org.apache.karaf.profile.command.completers,
                         </Export-Package>
                         <Private-Package>
+                            org.apache.karaf.profile.assembly,
                             org.apache.karaf.profile.command,
                             org.apache.karaf.profile.command.completers,
-                            org.apache.karaf.profile.assembly,
                             org.apache.karaf.profile.impl,
                             org.apache.karaf.profile.impl.osgi,
-                            org.apache.karaf.profile.versioning,
-                            org.apache.karaf.util,
-                            org.apache.karaf.util.config,
-                            org.apache.karaf.util.maven,
-                            org.apache.felix.utils.manifest,
-                            org.apache.felix.utils.version,
-                            org.apache.felix.utils.properties,
+                            org.apache.karaf.util;-split-package:=merge-first,
+                            org.apache.karaf.util.config;-split-package:=merge-first,
+                            org.apache.karaf.util.maven;-split-package:=merge-first,
+                            org.apache.karaf.util.tracker;-split-package:=merge-first,
+                            org.apache.felix.utils.manifest;-split-package:=merge-first,
+                            org.apache.felix.utils.properties;-split-package:=merge-first,
+                            org.apache.felix.utils.version;-split-package:=merge-first
                         </Private-Package>
                     </instructions>
                 </configuration>
diff --git a/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java b/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java
index f068f5e..f1b280f 100644
--- a/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java
+++ b/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java
@@ -48,13 +48,19 @@ public class ArtifactInstaller {
         this.downloader = downloader;
         this.blacklist = blacklist;
     }
-    
+
+    /**
+     * Installs a {@link BundleInfo} into <code>system/</code> directory taking into account <em>blacklisted</em>
+     * and <em>overriden</em> flags.
+     * @param bundle
+     * @throws Exception
+     */
     public void installArtifact(BundleInfo bundle) throws Exception {
         if (bundle.isBlacklisted()) {
             LOGGER.info("      skipping blacklisted maven artifact: " + bundle.getLocation());
             return;
         }
-        if (bundle.isOverriden()) {
+        if (bundle.isOverriden() != BundleInfo.BundleOverrideMode.NONE) {
             LOGGER.info("      adding overriden maven artifact: " + bundle.getLocation() + " (original location: " + bundle.getOriginalLocation() + ")");
         } else {
             LOGGER.info("      adding maven artifact: " + bundle.getLocation());
@@ -76,6 +82,12 @@ public class ArtifactInstaller {
         });
     }
 
+    /**
+     * Installs generic artifact to <code>system/</code> directory. For bundles, dedicated {@link #installArtifact(BundleInfo)}
+     * should be used.
+     * @param location
+     * @throws Exception
+     */
     public void installArtifact(String location) throws Exception {
         LOGGER.info("      adding maven artifact: " + location);
         location = removeTrailingSlash(stripUrl(location));
diff --git a/profile/src/main/java/org/apache/karaf/profile/assembly/AssemblyDeployCallback.java b/profile/src/main/java/org/apache/karaf/profile/assembly/AssemblyDeployCallback.java
index 6e854dc..c9c3560 100644
--- a/profile/src/main/java/org/apache/karaf/profile/assembly/AssemblyDeployCallback.java
+++ b/profile/src/main/java/org/apache/karaf/profile/assembly/AssemblyDeployCallback.java
@@ -26,6 +26,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Hashtable;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicLong;
@@ -33,6 +34,7 @@ import java.util.jar.Attributes;
 import java.util.jar.JarFile;
 
 import org.apache.felix.utils.properties.Properties;
+import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.DeploymentEvent;
 import org.apache.karaf.features.FeatureEvent;
 import org.apache.karaf.features.FeaturesService;
@@ -43,8 +45,8 @@ import org.apache.karaf.features.internal.model.ConfigFile;
 import org.apache.karaf.features.internal.model.Feature;
 import org.apache.karaf.features.internal.model.Features;
 import org.apache.karaf.features.internal.model.Library;
-import org.apache.karaf.features.internal.service.Blacklist;
 import org.apache.karaf.features.internal.service.Deployer;
+import org.apache.karaf.features.internal.service.FeaturesProcessor;
 import org.apache.karaf.features.internal.service.State;
 import org.apache.karaf.features.internal.service.StaticInstallSupport;
 import org.apache.karaf.features.internal.util.MapUtils;
@@ -56,48 +58,71 @@ import org.osgi.framework.wiring.BundleRevision;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+/**
+ * Callback through which {@link Deployer} will interact with the distribution that's being assembled.
+ */
 public class AssemblyDeployCallback extends StaticInstallSupport implements Deployer.DeployCallback {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(Builder.class);
 
     private final DownloadManager manager;
     private final Builder builder;
-    private Blacklist featureBlacklist;
-    private Blacklist bundleBlacklist;
     private final Path homeDirectory;
     private final int defaultStartLevel;
     private final Path etcDirectory;
     private final Path systemDirectory;
     private final Deployer.DeploymentState dstate;
     private final AtomicLong nextBundleId = new AtomicLong(0);
+    private final FeaturesProcessor processor;
 
     private final Map<String, Bundle> bundles = new HashMap<>();
 
-    public AssemblyDeployCallback(DownloadManager manager, Builder builder, BundleRevision systemBundle, Collection<Features> repositories) {
+    /**
+     * Create a {@link Deployer.DeployCallback} performing actions on runtime with single system bundle installed
+     * and with access to all non-blacklisted features.
+     * @param manager
+     * @param builder
+     * @param systemBundle
+     * @param repositories
+     * @param processor
+     */
+    public AssemblyDeployCallback(DownloadManager manager, Builder builder, BundleRevision systemBundle, Collection<Features> repositories,
+                                  FeaturesProcessor processor) {
         this.manager = manager;
         this.builder = builder;
-//        this.featureBlacklist = new Blacklist(builder.getBlacklistedFeatures());
-//        this.bundleBlacklist = new Blacklist(builder.getBlacklistedBundles());
         this.homeDirectory = builder.homeDirectory;
         this.etcDirectory = homeDirectory.resolve("etc");
         this.systemDirectory = homeDirectory.resolve("system");
         this.defaultStartLevel = builder.defaultStartLevel;
+        this.processor = processor;
+
         dstate = new Deployer.DeploymentState();
         dstate.bundles = new HashMap<>();
-        dstate.features = new HashMap<>();
         dstate.bundlesPerRegion = new HashMap<>();
         dstate.filtersPerRegion = new HashMap<>();
         dstate.state = new State();
 
         MapUtils.addToMapSet(dstate.bundlesPerRegion, FeaturesService.ROOT_REGION, 0l);
         dstate.bundles.put(0l, systemBundle.getBundle());
+
+        Collection<org.apache.karaf.features.Feature> features = new LinkedList<>();
         for (Features repo : repositories) {
+            if (repo.isBlacklisted()) {
+                continue;
+            }
             for (Feature f : repo.getFeature()) {
-                dstate.features.put(f.getId(), f);
+                if (!f.isBlacklisted()) {
+                    features.add(f);
+                }
             }
         }
+        dstate.partitionFeatures(features);
     }
 
+    /**
+     * Get startup bundles with related start-level
+     * @return
+     */
     public Map<String, Integer> getStartupBundles() {
         Map<String, Integer> startup = new HashMap<>();
         for (Map.Entry<String, Bundle> bundle : bundles.entrySet()) {
@@ -193,11 +218,11 @@ public class AssemblyDeployCallback extends StaticInstallSupport implements Depl
     }
     
     private void assertNotBlacklisted(org.apache.karaf.features.Feature feature) {
-//        if (featureBlacklist.isFeatureBlacklisted(feature.getName(), feature.getVersion())) {
-//            if (builder.getBlacklistPolicy() == Builder.BlacklistPolicy.Fail) {
-//                throw new RuntimeException("Feature " + feature.getId() + " is blacklisted");
-//            }
-//        }
+        if (feature.isBlacklisted()) {
+            if (builder.getBlacklistPolicy() == Builder.BlacklistPolicy.Fail) {
+                throw new RuntimeException("Feature " + feature.getId() + " is blacklisted");
+            }
+        }
     }
 
     @Override
@@ -211,11 +236,12 @@ public class AssemblyDeployCallback extends StaticInstallSupport implements Depl
     @Override
     public Bundle installBundle(String region, String uri, InputStream is) throws BundleException {
         // Check blacklist
-//        if (bundleBlacklist.isBundleBlacklisted(uri)) {
-//            if (builder.getBlacklistPolicy() == Builder.BlacklistPolicy.Fail) {
-//                throw new RuntimeException("Bundle " + uri + " is blacklisted");
-//            }
-//        }
+        if (processor.isBundleBlacklisted(uri)) {
+            if (builder.getBlacklistPolicy() == Builder.BlacklistPolicy.Fail) {
+                throw new RuntimeException("Bundle " + uri + " is blacklisted");
+            }
+        }
+
         // Install
         LOGGER.info("      adding maven artifact: " + uri);
         try {
@@ -226,7 +252,7 @@ public class AssemblyDeployCallback extends StaticInstallSupport implements Depl
                 path = Parser.pathFromMaven(uri);
             } else {
                 uri = uri.replaceAll("[^0-9a-zA-Z.\\-_]+", "_");
-		        if (uri.length() > 256) {
+                if (uri.length() > 256) {
                     //to avoid the File name too long exception
                     uri = uri.substring(0, 255);
                 }
@@ -261,6 +287,11 @@ public class AssemblyDeployCallback extends StaticInstallSupport implements Depl
         bundle.adapt(BundleStartLevel.class).setStartLevel(startLevel);
     }
 
+    @Override
+    public void bundleBlacklisted(BundleInfo bundleInfo) {
+        LOGGER.info("      skipping blacklisted bundle: {}", bundleInfo.getLocation());
+    }
+
     private String substFinalName(String finalname) {
         final String markerVarBeg = "${";
         final String markerVarEnd = "}";
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 c6ed79f..c032fe9 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
@@ -103,6 +103,7 @@ import org.ops4j.pax.url.mvn.MavenResolvers;
 import org.osgi.framework.Constants;
 import org.osgi.framework.wiring.BundleRevision;
 import org.osgi.resource.Resource;
+import org.osgi.service.repository.Repository;
 import org.osgi.service.resolver.Resolver;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -1278,6 +1279,9 @@ public class Builder {
                         Path input = provider.getFile().toPath();
                         String name = filename != null ? filename : input.getFileName().toString();
                         Path libOutput = homeDirectory.resolve(path).resolve(name);
+                        if (!libOutput.toFile().getParentFile().isDirectory()) {
+                            libOutput.toFile().getParentFile().mkdirs();
+                        }
                         LOGGER.info("{}   adding library: {}", indent, homeDirectory.relativize(libOutput));
                         Files.copy(input, libOutput, StandardCopyOption.REPLACE_EXISTING);
                         if (provider.getUrl().startsWith("mvn:")) {
@@ -1342,7 +1346,7 @@ public class Builder {
         Downloader downloader = manager.createDownloader();
 
         // Load startup repositories
-        LOGGER.info("   Loading repositories");
+        LOGGER.info("   Loading installed repositories");
         Map<String, Features> installedRepositories = loadRepositories(manager, installedEffective.getRepositories(), true, processor);
         // Compute startup feature dependencies
         Set<Feature> allInstalledFeatures = new HashSet<>();
@@ -1388,7 +1392,7 @@ public class Builder {
         Profile bootOverlay = Profiles.getOverlay(bootProfile, allProfiles, environment);
         Profile bootEffective = Profiles.getEffective(bootOverlay, false);
         // Load startup repositories
-        LOGGER.info("   Loading repositories");
+        LOGGER.info("   Loading boot repositories");
         Map<String, Features> bootRepositories = loadRepositories(manager, bootEffective.getRepositories(), true, processor);
         // Compute startup feature dependencies
         Set<Feature> allBootFeatures = new HashSet<>();
@@ -1605,21 +1609,24 @@ public class Builder {
         Profile startupOverlay = Profiles.getOverlay(startupProfile, allProfiles, environment);
         Profile startupEffective = Profiles.getEffective(startupOverlay, false);
         // Load startup repositories
-        LOGGER.info("   Loading repositories");
+        LOGGER.info("   Loading startup repositories");
         Map<String, Features> startupRepositories = loadRepositories(manager, startupEffective.getRepositories(), false, processor);
 
         //
         // Resolve
         //
-        LOGGER.info("   Resolving features");
+        LOGGER.info("   Resolving startup features and bundles");
+        LOGGER.info("      Features: " + startupEffective.getFeatures().stream().collect(Collectors.joining(", ")));
+        LOGGER.info("      Bundles: " + startupEffective.getBundles().stream().collect(Collectors.joining(", ")));
+
         Map<String, Integer> bundles =
                 resolve(manager,
                         resolver,
                         startupRepositories.values(),
                         startupEffective.getFeatures(),
                         startupEffective.getBundles(),
-                        startupEffective.getOverrides(),
-                        startupEffective.getOptionals());
+                        startupEffective.getOptionals(),
+                        processor);
 
         //
         // Generate startup.properties
@@ -1709,10 +1716,9 @@ public class Builder {
                             try (InputStream is = provider.open()) {
                                 Features featuresModel = JaxbUtil.unmarshal(url, is, false);
                                 // always process according to processor configuration
+                                featuresModel.setBlacklisted(processor.isRepositoryBlacklisted(url));
                                 processor.process(featuresModel);
-                                // TODO consult blacklist policy
-//                                if (blacklistPolicy == BlacklistPolicy.Discard) {
-//                                }
+
                                 loaded.put(provider.getUrl(), featuresModel);
                                 for (String innerRepository : featuresModel.getRepository()) {
                                     downloader.download(innerRepository, this);
@@ -1754,59 +1760,85 @@ public class Builder {
                 .getProfile();
     }
 
+    /**
+     * <p>Resolves set of features and bundles using OSGi resolver to calculate startup stage bundles.</p>
+     * <p>Startup stage means that <em>current</em> state of the OSGi framework is just single system bundle installed
+     * and bundles+features are being resolved against this single <em>bundle 0</em>.</p>
+     *
+     * @param manager {@link DownloadManager} to help downloading bundles and resources
+     * @param resolver OSGi resolver which will resolve features and bundles in framework with only system bundle installed
+     * @param repositories all available (not only to-be-installed) features
+     * @param features feature identifiers to resolve
+     * @param bundles bundle locations to resolve
+     * @param optionals optional URI locations that'll be available through {@link org.osgi.service.repository.Repository},
+     * used in resolution process
+     * @param processor {@link FeaturesProcessor} to process repositories/features/bundles
+     * @return map from bundle URI to bundle start-level
+     * @throws Exception
+     */
     private Map<String, Integer> resolve(
                     DownloadManager manager,
                     Resolver resolver,
                     Collection<Features> repositories,
                     Collection<String> features,
                     Collection<String> bundles,
-                    Collection<String> overrides,
-                    Collection<String> optionals) throws Exception {
+                    Collection<String> optionals,
+                    FeaturesProcessor processor) throws Exception {
+
+        // System bundle will be single bundle installed with bundleId == 0
         BundleRevision systemBundle = getSystemBundle();
-        AssemblyDeployCallback callback = new AssemblyDeployCallback(manager, this, systemBundle, repositories);
+        // Static distribution building callback and deployer that's used to deploy/collect startup-stage artifacts
+        AssemblyDeployCallback callback = new AssemblyDeployCallback(manager, this, systemBundle, repositories, processor);
         Deployer deployer = new Deployer(manager, resolver, callback);
 
         // Install framework
         Deployer.DeploymentRequest request = Deployer.DeploymentRequest.defaultDeploymentRequest();
-        // Add overrides
-        request.overrides.addAll(overrides);
-        // Add optional resources
-        final List<Resource> resources = new ArrayList<>();
-        Downloader downloader = manager.createDownloader();
-        for (String optional : optionals) {
-            downloader.download(optional, provider -> {
-                    Resource resource = ResourceBuilder.build(provider.getUrl(), getHeaders(provider));
-                    synchronized (resources) {
-                        resources.add(resource);
-                    }
-            });
-        }
-        downloader.await();
-        request.globalRepository = new BaseRepository(resources);
-        // Install features
+
+        // Add optional resources available through OSGi resource repository
+        request.globalRepository = repositoryOfOptionalResources(manager, optionals);
+
+        // Specify feature requirements (already prefixed with "feature:")
         for (String feature : features) {
             MapUtils.addToMapSet(request.requirements, FeaturesService.ROOT_REGION, feature);
         }
+        // Specify bundle requirements
         for (String bundle : bundles) {
             MapUtils.addToMapSet(request.requirements, FeaturesService.ROOT_REGION, "bundle:" + bundle);
         }
-        Set<String> prereqs = new HashSet<>();
-        while (true) {
-            try {
-                deployer.deploy(callback.getDeploymentState(), request);
-                break;
-            } catch (Deployer.PartialDeploymentException e) {
-                if (!prereqs.containsAll(e.getMissing())) {
-                    prereqs.addAll(e.getMissing());
-                } else {
-                    throw new Exception("Deployment aborted due to loop in missing prerequisites: " + e.getMissing());
-                }
-            }
-        }
+
+        deployer.deployFully(callback.getDeploymentState(), request);
 
         return callback.getStartupBundles();
     }
 
+    /**
+     * Optional resource URIs will be made available through OSGi {@link Repository}
+     * @param manager
+     * @param optionals
+     * @return
+     * @throws Exception
+     */
+    private Repository repositoryOfOptionalResources(DownloadManager manager, Collection<String> optionals)
+            throws Exception {
+        final List<Resource> resources = new ArrayList<>();
+        Downloader downloader = manager.createDownloader();
+        for (String optional : optionals) {
+            downloader.download(optional, provider -> {
+                Resource resource = ResourceBuilder.build(provider.getUrl(), getHeaders(provider));
+                synchronized (resources) {
+                    resources.add(resource);
+                }
+            });
+        }
+        downloader.await();
+        return new BaseRepository(resources);
+    }
+
+    /**
+     * Prepares {@link BundleRevision} that represents System Bundle (a.k.a. <em>bundle 0</em>)
+     * @return
+     * @throws Exception
+     */
     @SuppressWarnings("rawtypes")
     private BundleRevision getSystemBundle() throws Exception {
         Path configPropPath = etcDirectory.resolve("config.properties");
diff --git a/profile/src/main/java/org/apache/karaf/profile/assembly/ConfigInstaller.java b/profile/src/main/java/org/apache/karaf/profile/assembly/ConfigInstaller.java
index 13943a2..de40401 100644
--- a/profile/src/main/java/org/apache/karaf/profile/assembly/ConfigInstaller.java
+++ b/profile/src/main/java/org/apache/karaf/profile/assembly/ConfigInstaller.java
@@ -16,8 +16,6 @@
  */
 package org.apache.karaf.profile.assembly;
 
-import java.io.IOException;
-import java.net.MalformedURLException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
@@ -33,6 +31,9 @@ import org.apache.karaf.features.internal.model.Feature;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+/**
+ * Installs PID configuration to <code>${karaf.etc}</code> and <code>system/</code> directory.
+ */
 public class ConfigInstaller {
     private static final Logger LOGGER = LoggerFactory.getLogger(ConfigInstaller.class);
     private Path etcDirectory;
@@ -41,11 +42,10 @@ public class ConfigInstaller {
     public ConfigInstaller(Path etcDirectory, List<String> pidsToExtract) {
         this.etcDirectory = etcDirectory;
         this.pidsToExtract = pidsToExtract;
-        // TODO Auto-generated constructor stub
     }
 
     public void installConfigs(Feature feature, Downloader downloader, ArtifactInstaller installer)
-        throws Exception, MalformedURLException, IOException {
+        throws Exception {
         List<Content> contents = new ArrayList<>();
         contents.add(feature);
         contents.addAll(feature.getConditional());
diff --git a/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java
index 24dbe98..9ea1722 100644
--- a/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java
+++ b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java
@@ -310,7 +310,7 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
      * @return
      */
     private byte[] reformat(String name, FileContent fileContent) {
-        if (!fileContent.generated && !(isOverlay && name.equals(INTERNAL_PID + PROPERTIES_SUFFIX))) {
+        if (!fileContent.generated || !(isOverlay && name.equals(INTERNAL_PID + PROPERTIES_SUFFIX))) {
             return fileContent.bytes;
         }
 
diff --git a/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java b/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java
index 5a1e886..6369e38 100644
--- a/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java
+++ b/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java
@@ -89,7 +89,7 @@ public class ProfilesTest {
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void testProfilePlaceholderResolverWitCycle() {
+    public void testProfilePlaceholderResolverWithCycle() {
         Profile profile = ProfileBuilder.Factory.create("test")
                 .addConfiguration("pid1", "foo", "b${profile:pid2/bar}")
                 .addConfiguration("pid2", "bar", "a${rep}")
@@ -212,12 +212,13 @@ public class ProfilesTest {
         parents.put("p1", p1);
         parents.put("p2", p2);
 
-        assertThat(Profiles.getOverlay(c, parents).getAttributes().get("a"), equalTo("5"));
-        assertThat(Profiles.getOverlay(c, parents).getAttributes().get("b"), equalTo("4"));
-        assertThat(Profiles.getOverlay(c, parents).getAttributes().get("c"), equalTo("2"));
-        assertThat(Profiles.getOverlay(c, parents).getConfiguration("p").get("p"), equalTo("5"));
-        assertThat(Profiles.getOverlay(c, parents).getConfiguration("p").get("px"), equalTo("1"));
-        assertThat(Profiles.getOverlay(c, parents).getFileConfiguration("f"), equalTo(new byte[] { 0x05 }));
+        Profile overlay = Profiles.getOverlay(c, parents);
+        assertThat(overlay.getAttributes().get("a"), equalTo("5"));
+        assertThat(overlay.getAttributes().get("b"), equalTo("4"));
+        assertThat(overlay.getAttributes().get("c"), equalTo("2"));
+        assertThat(overlay.getConfiguration("p").get("p"), equalTo("5"));
+        assertThat(overlay.getConfiguration("p").get("px"), equalTo("1"));
+        assertThat(overlay.getFileConfiguration("f"), equalTo(new byte[] { 0x05 }));
     }
 
     @Test
diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java
index 3aca9d3..fce9e55 100644
--- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java
+++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java
@@ -31,7 +31,6 @@ import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -39,6 +38,7 @@ import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
@@ -59,6 +59,7 @@ import org.apache.felix.resolver.Logger;
 import org.apache.felix.resolver.ResolverImpl;
 import org.apache.felix.utils.version.VersionRange;
 import org.apache.felix.utils.version.VersionTable;
+import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.DeploymentEvent;
 import org.apache.karaf.features.FeatureEvent;
 import org.apache.karaf.features.FeaturesService;
@@ -86,7 +87,6 @@ import org.apache.karaf.tooling.utils.ReactorMavenResolver;
 import org.apache.karaf.util.config.PropertiesLoader;
 import org.apache.maven.artifact.Artifact;
 import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugin.MojoFailureException;
 import org.apache.maven.plugins.annotations.Component;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
@@ -98,7 +98,6 @@ import org.ops4j.pax.url.mvn.MavenResolvers;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleException;
 import org.osgi.framework.Constants;
-import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.framework.Version;
 import org.osgi.framework.namespace.IdentityNamespace;
 import org.osgi.framework.startlevel.BundleStartLevel;
@@ -162,7 +161,7 @@ public class VerifyMojo extends MojoSupport {
     protected MavenResolver resolver;
 
     @Override
-    public void execute() throws MojoExecutionException, MojoFailureException {
+    public void execute() throws MojoExecutionException {
         if (skip) {
             return;
         }
@@ -218,7 +217,7 @@ public class VerifyMojo extends MojoSupport {
         }
     }
 
-    protected void doExecute() throws MojoExecutionException, MojoFailureException {
+    protected void doExecute() throws MojoExecutionException {
         System.setProperty("karaf.home", "target/karaf");
         System.setProperty("karaf.data", "target/karaf/data");
 
@@ -431,7 +430,6 @@ public class VerifyMojo extends MojoSupport {
                 throw new MojoExecutionException("Unable to resolve framework features", e);
             }
 
-
             /*
             boolean resolveOptionalImports = getResolveOptionalImports(properties);
 
@@ -456,25 +454,13 @@ public class VerifyMojo extends MojoSupport {
             }
             */
 
-
             // Install features
             for (String feature : features) {
                 MapUtils.addToMapSet(request.requirements, FeaturesService.ROOT_REGION, feature);
             }
             try {
-                Set<String> prereqs = new HashSet<>();
-                while (true) {
-                    try {
-                        deployer.deploy(callback.getDeploymentState(), request);
-                        break;
-                    } catch (Deployer.PartialDeploymentException e) {
-                        if (!prereqs.containsAll(e.getMissing())) {
-                            prereqs.addAll(e.getMissing());
-                        } else {
-                            throw new Exception("Deployment aborted due to loop in missing prerequisites: " + e.getMissing());
-                        }
-                    }
-                }
+                deployer.deployFully(callback.getDeploymentState(), request);
+
                 // TODO: find unused resources ?
             } catch (Exception e) {
                 throw new MojoExecutionException("Feature resolution failed for " + features
@@ -482,8 +468,6 @@ public class VerifyMojo extends MojoSupport {
                         + "\nRepositories: " + toString(new TreeSet<>(repositories.keySet()))
                         + "\nResources: " + toString(new TreeSet<>(manager.getProviders().keySet())), e);
             }
-
-
         } catch (MojoExecutionException e) {
             throw e;
         } catch (Exception e) {
@@ -641,7 +625,7 @@ public class VerifyMojo extends MojoSupport {
                     new Class[] { Bundle.class },
                     new InvocationHandler() {
                         @Override
-                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+                        public Object invoke(Object proxy, Method method, Object[] args) {
                             if (method.getName().equals("hashCode")) {
                                 return FakeBundleRevision.this.hashCode();
                             } else if (method.getName().equals("equals")) {
@@ -741,22 +725,29 @@ public class VerifyMojo extends MojoSupport {
         private final Deployer.DeploymentState dstate;
         private final AtomicLong nextBundleId = new AtomicLong(0);
 
-        public DummyDeployCallback(Bundle sysBundle, Collection<Features> repositories) throws Exception {
+        public DummyDeployCallback(Bundle sysBundle, Collection<Features> repositories) {
             systemBundle = sysBundle;
             dstate = new Deployer.DeploymentState();
             dstate.bundles = new HashMap<>();
-            dstate.features = new HashMap<>();
             dstate.bundlesPerRegion = new HashMap<>();
             dstate.filtersPerRegion = new HashMap<>();
             dstate.state = new State();
 
             MapUtils.addToMapSet(dstate.bundlesPerRegion, FeaturesService.ROOT_REGION, 0l);
             dstate.bundles.put(0l, systemBundle);
+
+            Collection<org.apache.karaf.features.Feature> features = new LinkedList<>();
             for (Features repo : repositories) {
+                if (repo.isBlacklisted()) {
+                    continue;
+                }
                 for (Feature f : repo.getFeature()) {
-                    dstate.features.put(f.getId(), f);
+                    if (!f.isBlacklisted()) {
+                        features.add(f);
+                    }
                 }
             }
+            dstate.partitionFeatures(features);
         }
 
         public Deployer.DeploymentState getDeploymentState() {
@@ -769,15 +760,15 @@ public class VerifyMojo extends MojoSupport {
         }
 
         @Override
-        public void persistResolveRequest(Deployer.DeploymentRequest request) throws IOException {
+        public void persistResolveRequest(Deployer.DeploymentRequest request) {
         }
 
         @Override
-        public void installConfigs(org.apache.karaf.features.Feature feature) throws IOException, InvalidSyntaxException {
+        public void installConfigs(org.apache.karaf.features.Feature feature) {
         }
         
         @Override
-        public void installLibraries(org.apache.karaf.features.Feature feature) throws IOException {
+        public void installLibraries(org.apache.karaf.features.Feature feature) {
         }
 
         @Override
@@ -812,6 +803,11 @@ public class VerifyMojo extends MojoSupport {
             }
         }
 
+        @Override
+        public void bundleBlacklisted(BundleInfo bundleInfo) {
+
+        }
+
     }
 
     public class MavenResolverLog extends org.apache.felix.resolver.Logger {

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

[karaf] 07/11: [KARAF-5468] Add comments to generated target/assembly/etc/profile.cfg

Posted by gg...@apache.org.
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 59cb98589b83f932989d080ddefe9168d2cb2a64
Author: Grzegorz Grzybek <gr...@gmail.com>
AuthorDate: Fri Nov 17 12:58:20 2017 +0100

    [KARAF-5468] Add comments to generated target/assembly/etc/profile.cfg
---
 .../org/apache/karaf/profile/ProfileConstants.java |   5 +
 .../org/apache/karaf/profile/assembly/Builder.java |   2 -
 .../karaf/profile/impl/ProfileBuilderImpl.java     | 187 ++++++++++++++++++---
 .../apache/karaf/profile/impl/PropertiesTest.java  |  41 +++++
 4 files changed, 211 insertions(+), 24 deletions(-)

diff --git a/profile/src/main/java/org/apache/karaf/profile/ProfileConstants.java b/profile/src/main/java/org/apache/karaf/profile/ProfileConstants.java
index fe9417d..8df688b 100644
--- a/profile/src/main/java/org/apache/karaf/profile/ProfileConstants.java
+++ b/profile/src/main/java/org/apache/karaf/profile/ProfileConstants.java
@@ -46,6 +46,11 @@ public interface ProfileConstants {
     String HIDDEN = "hidden";
 
     /**
+     * The attribute key for the <em>overlay</em> flag meaning the parents are already included/merged in the profile.
+     */
+    String OVERLAY = "overlay";
+
+    /**
      * <p>Key indicating a deletion.</p>
      * <p>This value can appear as the value of a key in a configuration
      * or as a key itself.  If used as a key, the whole configuration
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 6df4243..672d166 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
@@ -788,7 +788,6 @@ public class Builder {
         // Unzip KARs
         //
         LOGGER.info("Unzipping kars");
-//        Map<String, RepositoryInfo> repositories = new LinkedHashMap<>(this.repositories);
         Downloader downloader = manager.createDownloader();
         for (String kar : kars.keySet()) {
             downloader.download(kar, null);
@@ -811,7 +810,6 @@ public class Builder {
         // Propagate feature installation from repositories
         //
         LOGGER.info("Loading repositories");
-//        Map<String, Stage> features = new LinkedHashMap<>(this.features);
         Map<String, Features> karRepositories = loadRepositories(manager, repositories.keySet(), false);
         for (String repo : repositories.keySet()) {
             RepositoryInfo info = repositories.get(repo);
diff --git a/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java
index 10331bd..24dbe98 100644
--- a/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java
+++ b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java
@@ -22,15 +22,18 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 
+import org.apache.felix.utils.properties.TypedProperties;
 import org.apache.karaf.profile.Profile;
 import org.apache.karaf.profile.ProfileBuilder;
 
+import static org.apache.karaf.profile.ProfileConstants.*;
 import static org.apache.karaf.profile.impl.ProfileImpl.ConfigListType;
 
 /**
@@ -39,7 +42,7 @@ import static org.apache.karaf.profile.impl.ProfileImpl.ConfigListType;
 public final class ProfileBuilderImpl implements ProfileBuilder {
 
 	private String profileId;
-	private Map<String, byte[]> fileMapping = new HashMap<>();
+	private Map<String, FileContent> fileMapping = new HashMap<>();
 	private boolean isOverlay;
 	
 	@Override
@@ -58,7 +61,7 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
 	@Override
     public List<String> getParents() {
         Map<String, Object> config = getConfigurationInternal(Profile.INTERNAL_PID);
-        String pspec = (String) config.get(Profile.PARENTS);
+        String pspec = (String) config.get(PARENTS);
         String[] parentIds = pspec != null ? pspec.split(" ") : new String[0];
         return Arrays.asList(parentIds);
     }
@@ -100,9 +103,9 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
 
     private void updateParentsAttribute(Collection<String> parentIds) {
         Map<String, Object> config = getConfigurationInternal(Profile.INTERNAL_PID);
-        config.remove(Profile.PARENTS);
+        config.remove(PARENTS);
         if (parentIds.size() > 0) {
-            config.put(Profile.PARENTS, parentsAttributeValue(parentIds));
+            config.put(PARENTS, parentsAttributeValue(parentIds));
         }
         addConfiguration(Profile.INTERNAL_PID, config);
     }
@@ -118,18 +121,19 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
 
     @Override
     public byte[] getFileConfiguration(String key) {
-        return fileMapping.get(key);
+        return fileMapping.get(key) == null ? null : fileMapping.get(key).bytes;
     }
 
-	@Override
-	public ProfileBuilder setFileConfigurations(Map<String, byte[]> configurations) {
-		fileMapping = new HashMap<>(configurations);
-		return this;
-	}
+    @Override
+    public ProfileBuilder setFileConfigurations(Map<String, byte[]> configurations) {
+        fileMapping = new HashMap<>();
+        configurations.forEach((name, bytes) -> fileMapping.put(name, new FileContent(bytes, false)));
+        return this;
+    }
 
     @Override
     public ProfileBuilder addFileConfiguration(String fileName, byte[] data) {
-        fileMapping.put(fileName, data);
+        fileMapping.put(fileName, new FileContent(data, false));
         return this;
     }
 
@@ -152,7 +156,7 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
 
     @Override
     public ProfileBuilder addConfiguration(String pid, Map<String, Object> config) {
-        fileMapping.put(pid + Profile.PROPERTIES_SUFFIX, Utils.toBytes(config));
+        fileMapping.put(pid + Profile.PROPERTIES_SUFFIX, new FileContent(Utils.toBytes(config), true));
         return this;
     }
 
@@ -181,8 +185,8 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
     }
 
     private Map<String, Object> getConfigurationInternal(String pid) {
-        byte[] bytes = fileMapping.get(pid + Profile.PROPERTIES_SUFFIX);
-        return Utils.toProperties(bytes);
+        FileContent content = fileMapping.get(pid + Profile.PROPERTIES_SUFFIX);
+        return Utils.toProperties(content == null ? null : content.bytes);
     }
     
     @Override
@@ -239,10 +243,11 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
         return this;
     }
 
-	public ProfileBuilder setOverlay(boolean overlay) {
-		this.isOverlay = overlay;
-		return this;
-	}
+    public ProfileBuilder setOverlay(boolean overlay) {
+        this.isOverlay = overlay;
+        addConfiguration(Profile.INTERNAL_PID, Profile.ATTRIBUTE_PREFIX + Profile.OVERLAY, Boolean.toString(overlay));
+        return this;
+    }
 
 	@Override
     public ProfileBuilder addAttribute(String key, String value) {
@@ -286,10 +291,148 @@ public final class ProfileBuilderImpl implements ProfileBuilder {
         addConfiguration(Profile.INTERNAL_PID, config);
     }
 
-
+    /**
+     * Returns an immutable implementation of {@link Profile}
+     * @return
+     */
     @Override
-	public Profile getProfile() {
-		return new ProfileImpl(profileId, getParents(), fileMapping, isOverlay);
-	}
+    public Profile getProfile() {
+        // reformatting all generated files.
+        Map<String, byte[]> files = new LinkedHashMap<>();
+        fileMapping.forEach((k, v) -> files.put(k, reformat(k, v)));
+        return new ProfileImpl(profileId, getParents(), files, isOverlay);
+    }
+
+    /**
+     * If some properties file has been marked as {@link FileContent#generated}, then we can add some comment hints.
+     * @param name
+     * @param fileContent
+     * @return
+     */
+    private byte[] reformat(String name, FileContent fileContent) {
+        if (!fileContent.generated && !(isOverlay && name.equals(INTERNAL_PID + PROPERTIES_SUFFIX))) {
+            return fileContent.bytes;
+        }
+
+        TypedProperties properties = Utils.toProperties(fileContent.bytes);
+        TypedProperties result = Utils.toProperties((byte[])null);
+
+        String parents = null;
+        Map<String, Object> attributes = new LinkedHashMap<>();
+        Map<String, Object> repositories = new LinkedHashMap<>();
+        Map<String, Object> features = new LinkedHashMap<>();
+        Map<String, Object> bundles = new LinkedHashMap<>();
+        Map<String, Object> libraries = new LinkedHashMap<>();
+        Map<String, Object> bootLibraries = new LinkedHashMap<>();
+        Map<String, Object> endorsedLibraries = new LinkedHashMap<>();
+        Map<String, Object> extLibraries = new LinkedHashMap<>();
+        Map<String, Object> config = new LinkedHashMap<>();
+        Map<String, Object> system = new LinkedHashMap<>();
+        Map<String, Object> overrides = new LinkedHashMap<>();
+        Map<String, Object> optionals = new LinkedHashMap<>();
+        for (String key : properties.keySet()) {
+            Object v = properties.get(key);
+            if (key.equals(PARENTS)) {
+                parents = (String) v;
+            } else if (key.startsWith(ATTRIBUTE_PREFIX)) {
+                attributes.put(key, v);
+            } else if (key.startsWith(REPOSITORY_PREFIX)) {
+                repositories.put(key, v);
+            } else if (key.startsWith(FEATURE_PREFIX)) {
+                features.put(key, v);
+            } else if (key.startsWith(BUNDLE_PREFIX)) {
+                bundles.put(key, v);
+            } else if (key.startsWith(LIB_PREFIX)) {
+                libraries.put(key, v);
+            } else if (key.startsWith(BOOT_PREFIX)) {
+                bootLibraries.put(key, v);
+            } else if (key.startsWith(ENDORSED_PREFIX)) {
+                endorsedLibraries.put(key, v);
+            } else if (key.startsWith(EXT_PREFIX)) {
+                extLibraries.put(key, v);
+            } else if (key.startsWith(CONFIG_PREFIX)) {
+                config.put(key, v);
+            } else if (key.startsWith(SYSTEM_PREFIX)) {
+                system.put(key, v);
+            } else if (key.startsWith(OVERRIDE_PREFIX)) {
+                overrides.put(key, v);
+            } else if (key.startsWith(OPTIONAL_PREFIX)) {
+                optionals.put(key, v);
+            }
+        }
+
+        result.setHeader(Arrays.asList("#", "# Profile generated by Karaf Assembly Builder", "#"));
+        if (parents != null) {
+            result.put(PARENTS, comment("Parent profiles"), parents);
+        }
+        addGroupOfProperties("Attributes", result, attributes);
+        addGroupOfProperties("Feature XML repositories", result, repositories);
+        addGroupOfProperties("Features", result, features);
+        addGroupOfProperties("Bundles", result, bundles);
+        addGroupOfProperties("Libraries", result, libraries);
+        addGroupOfProperties("Boot libraries", result, bootLibraries);
+        addGroupOfProperties("Endorsed libraries", result, endorsedLibraries);
+        addGroupOfProperties("Extension libraries", result, extLibraries);
+        addGroupOfProperties("Configuration properties for etc/config.properties", result, config);
+        addGroupOfProperties("Configuration properties for etc/system.properties", result, system);
+        addGroupOfProperties("Bundle overrides (deprecated)", result, overrides);
+        addGroupOfProperties("Optional resources for resolution", result, optionals);
+
+        return Utils.toBytes(result);
+    }
+
+    /**
+     * Puts properties under single comment.
+     * @param comment
+     * @param properties
+     * @param values
+     */
+    private void addGroupOfProperties(String comment, TypedProperties properties, Map<String, Object> values) {
+        boolean first = true;
+        for (Entry<String, Object> entry : values.entrySet()) {
+            if (first) {
+                first = false;
+                properties.put(entry.getKey(), comment(comment), entry.getValue());
+            } else {
+                properties.put(entry.getKey(), entry.getValue());
+            }
+        }
+    }
+
+    /**
+     * Helper method to generate comments above property groups in {@link TypedProperties}
+     * @param comment
+     * @return
+     */
+    private List<String> comment(String comment) {
+        return Arrays.asList("", "# " + comment);
+    }
+
+    /**
+     * We can distinguish between bytes read from external file and bytes from serialized
+     * {@link org.apache.felix.utils.properties.TypedProperties}
+     */
+    static class FileContent {
+        byte[] bytes;
+        boolean generated;
+
+        public FileContent(byte[] bytes, boolean generated) {
+            this.bytes = bytes;
+            this.generated = generated;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            FileContent that = (FileContent) o;
+            return Arrays.equals(bytes, that.bytes);
+        }
+
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(bytes);
+        }
+    }
 
 }
diff --git a/profile/src/test/java/org/apache/karaf/profile/impl/PropertiesTest.java b/profile/src/test/java/org/apache/karaf/profile/impl/PropertiesTest.java
new file mode 100644
index 0000000..a8a0edc
--- /dev/null
+++ b/profile/src/test/java/org/apache/karaf/profile/impl/PropertiesTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.profile.impl;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.apache.felix.utils.properties.TypedProperties;
+import org.junit.Test;
+
+public class PropertiesTest {
+
+    @Test
+    public void typedProperties() throws IOException {
+        TypedProperties tp = new TypedProperties();
+        tp.put("commented-key", "# commented-key comment", "value");
+        tp.put("commented-key2", Arrays.asList("# ca", "# cb"), "value");
+        tp.put("normal-key", "value");
+        tp.put("listed-key", Arrays.asList("# ca", "# cb"), Arrays.asList("va", "vb"));
+        tp.setHeader(Arrays.asList("#", "# ASF License 2.0", "#"));
+        tp.setFooter(Arrays.asList("#", "# bye!", "#"));
+        tp.save(System.out);
+    }
+
+}

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

[karaf] 09/11: [KARAF-5376] Generate/merge features processor configuration from external and Maven config, improve logging in karaf-maven-plugin:assembly

Posted by gg...@apache.org.
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 abd345a1def8319fb54a9dfd25bda8bc9088af13
Author: Grzegorz Grzybek <gr...@gmail.com>
AuthorDate: Wed Nov 22 15:09:01 2017 +0100

    [KARAF-5376] Generate/merge features processor configuration from external and Maven config, improve logging in karaf-maven-plugin:assembly
---
 .../org/apache/karaf/features/FeaturePattern.java  | 12 +++
 .../org/apache/karaf/features/LocationPattern.java |  4 +
 .../model/processing/FeaturesProcessing.java       | 58 +++++++------
 .../karaf/features/internal/service/Blacklist.java | 12 +++
 .../karaf/profile/assembly/ArtifactInstaller.java  | 28 ++++++
 .../org/apache/karaf/profile/assembly/Builder.java | 99 ++++++++++++++--------
 .../org/apache/karaf/tooling/AssemblyMojo.java     | 10 +++
 7 files changed, 164 insertions(+), 59 deletions(-)

diff --git a/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java b/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java
index 107993b..a77c4df 100644
--- a/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java
+++ b/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java
@@ -80,6 +80,18 @@ public class FeaturePattern {
         }
     }
 
+    public String getOriginalFeatureId() {
+        return originalId;
+    }
+
+    public String getName() {
+        return nameString;
+    }
+
+    public String getVersion() {
+        return versionString;
+    }
+
     /**
      * Returns <code>true</code> if this feature pattern matches given feature/version
      * @param featureName
diff --git a/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java b/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
index 428746f..e5c96eb 100644
--- a/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
+++ b/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
@@ -108,6 +108,10 @@ public class LocationPattern {
         }
     }
 
+    public String getOriginalUri() {
+        return originalUri;
+    }
+
     /**
      * Converts a String with one special character (<code>*</code>) into working {@link Pattern}
      * @param value
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java
index 8a3c7b6..3f5050b 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java
@@ -142,22 +142,49 @@ public class FeaturesProcessing {
     }
 
     /**
-     * Perform <em>compilation</em> of rules declared in feature processing XML file.
+     * <p>Perform <em>compilation</em> of rules declared in feature processing XML file.</p>
+     * <p>Additional blacklist and overrides definitions will be added to this model</p>
+     *
      * @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()) {
+        // configure Blacklist tool
+        List<String> blacklisted = new LinkedList<>();
+
+        // compile blacklisted repository URIs (from XML and additional blacklist)
+        blacklist.getRepositoryBlacklist().stream()
+                .map(LocationPattern::getOriginalUri)
+                .forEach(uri -> getBlacklistedRepositories().add(uri));
+        for (String repositoryURI : getBlacklistedRepositories()) {
             try {
                 blacklistedRepositoryLocationPatterns.add(new LocationPattern(repositoryURI));
+                blacklisted.add(repositoryURI + ";" + Blacklist.BLACKLIST_TYPE + "=" + Blacklist.TYPE_REPOSITORY);
             } 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(); ) {
+        // add external blacklisted features to this model
+        blacklist.getFeatureBlacklist()
+                .forEach(fb -> getBlacklistedFeatures().add(new BlacklistedFeature(fb.getName(), fb.getVersion())));
+        blacklisted.addAll(getBlacklistedFeatures().stream()
+                .map(bf -> bf.getName() + ";" + Blacklist.BLACKLIST_TYPE + "=" + Blacklist.TYPE_FEATURE + (bf.getVersion() == null ? "" : ";" + FeaturePattern.RANGE + "=\"" + bf.getVersion() + "\""))
+                .collect(Collectors.toList()));
+
+        // add external blacklisted bundle URIs to this model
+        blacklist.getBundleBlacklist().stream()
+                .map(LocationPattern::getOriginalUri)
+                .forEach(uri -> getBlacklistedBundles().add(uri));
+        blacklisted.addAll(getBlacklistedBundles().stream()
+                .map(bl -> bl + ";" + Blacklist.BLACKLIST_TYPE + "=" + Blacklist.TYPE_BUNDLE)
+                .collect(Collectors.toList()));
+
+        this.blacklist = new Blacklist(blacklisted);
+
+        // verify bundle override definitions (from XML and additional overrides)
+        bundleReplacements.getOverrideBundles().addAll(parseOverridesClauses(overrides));
+        for (Iterator<BundleReplacements.OverrideBundle> iterator = 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
@@ -180,27 +207,6 @@ public class FeaturesProcessing {
                 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 + ";" + Blacklist.BLACKLIST_TYPE + "=" + Blacklist.TYPE_BUNDLE);
-        }
-        // blacklisted features - XML type to String instruction for Blacklist class
-        blacklisted.addAll(this.getBlacklistedFeatures().stream()
-                .map(bf -> bf.getName() + ";" + Blacklist.BLACKLIST_TYPE + "=" + Blacklist.TYPE_FEATURE + (bf.getVersion() == null ? "" : ";" + FeaturePattern.RANGE + "=\"" + bf.getVersion() + "\""))
-                .collect(Collectors.toList()));
-        // blacklisted repositories
-        for (String bl : this.getBlacklistedRepositories()) {
-            blacklisted.add(bl + ";" + Blacklist.BLACKLIST_TYPE + "=" + Blacklist.TYPE_REPOSITORY);
-        }
-
-        this.blacklist = new Blacklist(blacklisted);
-        this.blacklist.merge(blacklist);
-
-        // etc/overrides.properties (mvn: URIs)
-        bundleReplacements.getOverrideBundles().addAll(parseOverridesClauses(overrides));
     }
 
     /**
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 dcc1a20..9792afa 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
@@ -243,4 +243,16 @@ public class Blacklist {
         bundleBlacklist.add(locationPattern);
     }
 
+    public List<LocationPattern> getRepositoryBlacklist() {
+        return repositoryBlacklist;
+    }
+
+    public List<FeaturePattern> getFeatureBlacklist() {
+        return featureBlacklist;
+    }
+
+    public List<LocationPattern> getBundleBlacklist() {
+        return bundleBlacklist;
+    }
+
 }
diff --git a/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java b/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java
index 967a979..f068f5e 100644
--- a/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java
+++ b/profile/src/main/java/org/apache/karaf/profile/assembly/ArtifactInstaller.java
@@ -23,6 +23,7 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
 
+import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.internal.download.Downloader;
 import org.apache.karaf.features.internal.service.Blacklist;
 import org.apache.karaf.util.maven.Parser;
@@ -48,6 +49,33 @@ public class ArtifactInstaller {
         this.blacklist = blacklist;
     }
     
+    public void installArtifact(BundleInfo bundle) throws Exception {
+        if (bundle.isBlacklisted()) {
+            LOGGER.info("      skipping blacklisted maven artifact: " + bundle.getLocation());
+            return;
+        }
+        if (bundle.isOverriden()) {
+            LOGGER.info("      adding overriden maven artifact: " + bundle.getLocation() + " (original location: " + bundle.getOriginalLocation() + ")");
+        } else {
+            LOGGER.info("      adding maven artifact: " + bundle.getLocation());
+        }
+        String location = bundle.getLocation().trim();
+        location = removeTrailingSlash(stripUrl(location));
+        if (!location.startsWith("mvn:")) {
+            LOGGER.warn("Ignoring non maven artifact " + location);
+            return;
+        }
+        final String finalLocation = location;
+        downloader.download(location, provider -> {
+            String uri = provider.getUrl();
+            Path path = pathFromProviderUrl(systemDirectory, finalLocation);
+            synchronized (provider) {
+                Files.createDirectories(path.getParent());
+                Files.copy(provider.getFile().toPath(), path, StandardCopyOption.REPLACE_EXISTING);
+            }
+        });
+    }
+
     public void installArtifact(String location) throws Exception {
         LOGGER.info("      adding maven artifact: " + location);
         location = removeTrailingSlash(stripUrl(location));
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 1c77a84..0f7e747 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
@@ -62,6 +62,7 @@ import java.util.zip.ZipInputStream;
 import org.apache.felix.resolver.ResolverImpl;
 import org.apache.felix.utils.manifest.Clause;
 import org.apache.felix.utils.properties.Properties;
+import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.FeaturePattern;
 import org.apache.karaf.features.FeaturesService;
 import org.apache.karaf.features.Library;
@@ -286,6 +287,7 @@ public class Builder {
     boolean ignoreDependencyFlag;
     int defaultStartLevel = 50;
     Path homeDirectory;
+    Path featuresProcessingLocation;
     boolean offline;
     String localRepository;
     String mavenRepositories;
@@ -575,6 +577,20 @@ public class Builder {
     }
 
     /**
+     * <p>Configures custom location for a file with features processing instructions. Normally this file is generated
+     * by the builder if any of blacklisted options are configured.</p>
+     * <p>If custom location is provided and it's not <code>etc/org.apache.karaf.features.xml</code>, it is copied</p>
+     * <p>If custom location is provided and it's <code>etc/org.apache.karaf.features.xml</code>, it's left as is</p>
+     * <p>Any additional blacklisting/overrides configuration via Maven configuration causes overwrite of original
+     * content.</p>
+     * @param featuresProcessing
+     */
+    public Builder setFeaturesProcessing(Path featuresProcessing) {
+        this.featuresProcessingLocation = featuresProcessing;
+        return this;
+    }
+
+    /**
      * Ignore the dependency attribute (dependency="[true|false]") on bundles, effectively forcing their
      * installation.
      */
@@ -860,11 +876,26 @@ public class Builder {
         // profiles are generated after reading features from repositories
         // so for now, we can only configure blacklisting features processor
 
-        Path existingProcessorDefinition = etcDirectory.resolve("org.apache.karaf.features.xml");
+        boolean needFeaturesProcessorFileCopy = false;
         String existingProcessorDefinitionURI = null;
+        Path existingProcessorDefinition = etcDirectory.resolve("org.apache.karaf.features.xml");
         if (existingProcessorDefinition.toFile().isFile()) {
             existingProcessorDefinitionURI = existingProcessorDefinition.toFile().toURI().toString();
+            LOGGER.info("Found existing features processor configuration: {}", homeDirectory.relativize(existingProcessorDefinition));
+        }
+        if (featuresProcessingLocation != null && featuresProcessingLocation.toFile().isFile()
+                && !featuresProcessingLocation.equals(existingProcessorDefinition)) {
+            if (existingProcessorDefinitionURI != null) {
+                LOGGER.warn("Explicitly configured {} will be used for features processor configuration.", homeDirectory.relativize(featuresProcessingLocation));
+            } else {
+                LOGGER.info("Found features processor configuration: {}", homeDirectory.relativize(featuresProcessingLocation));
+            }
+            existingProcessorDefinitionURI = featuresProcessingLocation.toFile().toURI().toString();
+            // when there are no other (configured via Maven for example) processing instructions (e.g., blacklisting)
+            // we don't have to generate this file and may take original content
+            needFeaturesProcessorFileCopy = true;
         }
+
         // now we can configure blacklisting features processor which may have already defined (in XML)
         // configuration for bundle replacements or feature overrides.
         // we'll add overrides from profiles later.
@@ -1014,30 +1045,18 @@ public class Builder {
             editor.run();
         }
 
-//        if (!blacklistedFeatures.isEmpty() || !blacklistedBundles.isEmpty()) {
-//            List<String> lines = new ArrayList<>();
-//            lines.add("#");
-//            lines.add("# Generated by the karaf assembly builder");
-//            lines.add("#");
-//            if (!blacklistedFeatures.isEmpty()) {
-//                lines.add("");
-//                lines.add("# Features");
-//                lines.addAll(blacklistedFeatures);
-//            }
-//            if (!blacklistedBundles.isEmpty()) {
-//                lines.add("");
-//                lines.add("# Bundles");
-//                lines.addAll(blacklistedBundles);
-//            }
-//            LOGGER.info("Generating {}", homeDirectory.relativize(blacklist));
-//            Files.write(blacklist, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
-//        }
-
-        // TODO: download overrides, implement fuller override clauses (original->replacement)
         if (processor.hasInstructions()) {
             Path featuresProcessingXml = etcDirectory.resolve("org.apache.karaf.features.xml");
-            try (FileOutputStream fos = new FileOutputStream(featuresProcessingXml.toFile())) {
-                processor.writeInstructions(fos);
+            if (hasOwnInstructions() || overrides.size() > 0) {
+                // just generate new etc/org.apache.karaf.features.xml file (with external config + builder config)
+                try (FileOutputStream fos = new FileOutputStream(featuresProcessingXml.toFile())) {
+                    LOGGER.info("Generating features processor configuration: {}", homeDirectory.relativize(featuresProcessingXml));
+                    processor.writeInstructions(fos);
+                }
+            } else if (needFeaturesProcessorFileCopy) {
+                // we may simply copy configured features processor XML configuration
+                LOGGER.info("Copying features processor configuration: {} -> {}", homeDirectory.relativize(featuresProcessingLocation), homeDirectory.relativize(featuresProcessingXml));
+                Files.copy(featuresProcessingLocation, featuresProcessingXml, StandardCopyOption.REPLACE_EXISTING);
             }
         }
 
@@ -1058,6 +1077,20 @@ public class Builder {
     }
 
     /**
+     * Similar to {@link FeaturesProcessorImpl#hasInstructions()}, we check if there are any builder configuration
+     * options for blacklisted repos/features/bundles or overwrites.
+     * @return
+     */
+    private boolean hasOwnInstructions() {
+        int count = 0;
+        count += blacklistedRepositoryURIs.size();
+        count += blacklistedFeatureIdentifiers.size();
+        count += blacklistedBundleURIs.size();
+
+        return count > 0;
+    }
+
+    /**
      * Checks existing (etc/overrides.properties) and configured (in profiles) overrides definitions
      * @param profileOverrides
      * @return
@@ -1091,7 +1124,7 @@ public class Builder {
             try {
                 blacklist.blacklistRepository(new LocationPattern(br));
             } catch (MalformedURLException e) {
-                LOGGER.warn("Blacklisted features XML repository URI is invalid {}, ignoring", br);
+                LOGGER.warn("Blacklisted features XML repository URI is invalid: {}, ignoring", br);
             }
         }
         for (String bf : blacklistedFeatureIdentifiers) {
@@ -1101,7 +1134,7 @@ public class Builder {
             try {
                 blacklist.blacklistBundle(new LocationPattern(bb));
             } catch (MalformedURLException e) {
-                LOGGER.warn("Blacklisted bundle URI is invalid {}, ignoring", bb);
+                LOGGER.warn("Blacklisted bundle URI is invalid: {}, ignoring", bb);
             }
         }
         if (existingBlacklist != null) {
@@ -1320,7 +1353,7 @@ public class Builder {
             LOGGER.info("   Feature {} is defined as an installed feature", feature.getId());
             for (Bundle bundle : feature.getBundle()) {
                 if (!ignoreDependencyFlag || !bundle.isDependency()) {
-                    installer.installArtifact(bundle.getLocation().trim());
+                    installer.installArtifact(bundle);
                 }
             }
             // Install config files
@@ -1330,7 +1363,7 @@ public class Builder {
             for (Conditional cond : feature.getConditional()) {
                 for (Bundle bundle : cond.getBundle()) {
                     if (!ignoreDependencyFlag || !bundle.isDependency()) {
-                        installer.installArtifact(bundle.getLocation().trim());
+                        installer.installArtifact(bundle);
                     }
                 }
             }
@@ -1392,16 +1425,16 @@ public class Builder {
             // the feature is a startup feature, updating startup.properties file
             LOGGER.info("   Feature " + feature.getId() + " is defined as a boot feature");
             // add the feature in the system folder
-            Set<String> locations = new HashSet<>();
+            Set<BundleInfo> bundleInfos = new HashSet<>();
             for (Bundle bundle : feature.getBundle()) {
                 if (!ignoreDependencyFlag || !bundle.isDependency()) {
-                    locations.add(bundle.getLocation().trim());
+                    bundleInfos.add(bundle);
                 }
             }
             for (Conditional cond : feature.getConditional()) {
                 for (Bundle bundle : cond.getBundle()) {
                     if (!ignoreDependencyFlag || !bundle.isDependency()) {
-                        locations.add(bundle.getLocation().trim());
+                        bundleInfos.add(bundle);
                     }
                 }
             }
@@ -1413,10 +1446,10 @@ public class Builder {
             prereqs.put("wrap:", Collections.singletonList("wrap"));
             prereqs.put("war:", Collections.singletonList("war"));
             ArtifactInstaller installer = new ArtifactInstaller(systemDirectory, downloader, blacklist);
-            for (String location : locations) {
-                installer.installArtifact(location);
+            for (BundleInfo bundleInfo : bundleInfos) {
+                installer.installArtifact(bundleInfo);
                 for (Map.Entry<String, List<String>> entry : prereqs.entrySet()) {
-                    if (location.startsWith(entry.getKey())) {
+                    if (bundleInfo.getLocation().trim().startsWith(entry.getKey())) {
                         for (String prereq : entry.getValue()) {
                             Dependency dep = generatedDep.get(prereq);
                             if (dep == null) {
diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java
index 08f41cb..2830306 100644
--- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java
+++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/AssemblyMojo.java
@@ -88,6 +88,13 @@ public class AssemblyMojo extends MojoSupport {
     @Parameter(defaultValue = "${project.build.directory}/assembly")
     protected File workDirectory;
 
+    /**
+     * Optional location for custom features processing XML configuration
+     * (<code>etc/org.apache.karaf.features.cfg</code>)
+     */
+    @Parameter
+    protected File featuresProcessing;
+
     /*
      * There are three builder stages related to maven dependency scopes:
      *  - Stage.Startup : scope=compile
@@ -462,6 +469,9 @@ public class AssemblyMojo extends MojoSupport {
         builder.writeProfiles(writeProfiles);
         builder.environment(environment);
         builder.defaultStartLevel(defaultStartLevel);
+        if (featuresProcessing != null) {
+            builder.setFeaturesProcessing(featuresProcessing.toPath());
+        }
 
         // Set up remote repositories from Maven build, to be used by pax-url-aether resolver
         String remoteRepositories = MavenUtil.remoteRepositoryList(project.getRemoteProjectRepositories());

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

[karaf] 03/11: [KARAF-5376] Review BuilderTest

Posted by gg...@apache.org.
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 032a71afe7176bb0fbe0df1de07d881f7fddbc45
Author: Grzegorz Grzybek <gr...@gmail.com>
AuthorDate: Tue Nov 7 13:23:17 2017 +0100

    [KARAF-5376] Review BuilderTest
---
 profile/pom.xml                                    |  2 +-
 .../apache/karaf/profile/assembly/BuilderTest.java | 28 ++++++++++++++++++++++
 profile/src/test/resources/log4j.properties        | 26 ++++++++++++++++++++
 3 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/profile/pom.xml b/profile/pom.xml
index c50d3dc..223e522 100644
--- a/profile/pom.xml
+++ b/profile/pom.xml
@@ -134,7 +134,7 @@
 
         <dependency>
             <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-jdk14</artifactId>
+            <artifactId>slf4j-log4j12</artifactId>
             <scope>test</scope>
         </dependency>
 
diff --git a/profile/src/test/java/org/apache/karaf/profile/assembly/BuilderTest.java b/profile/src/test/java/org/apache/karaf/profile/assembly/BuilderTest.java
index 3b82371..8003e56 100644
--- a/profile/src/test/java/org/apache/karaf/profile/assembly/BuilderTest.java
+++ b/profile/src/test/java/org/apache/karaf/profile/assembly/BuilderTest.java
@@ -35,6 +35,34 @@ import static org.hamcrest.MatcherAssert.assertThat;
 public class BuilderTest {
 
     @Test
+    public void testAssemblyGoalLikeBuild() throws Exception {
+        Path workDir = Paths.get("target/assembly");
+        recursiveDelete(workDir);
+
+        // Create dummy etc/config.properties file
+        Path config = workDir.resolve("etc/config.properties");
+        Files.createDirectories(config.getParent());
+        try (BufferedWriter w = Files.newBufferedWriter(config)) {
+            w.write(Constants.FRAMEWORK_SYSTEMPACKAGES + " = org.osgi.dto");
+            w.newLine();
+            w.write(Constants.FRAMEWORK_SYSTEMCAPABILITIES + " = ");
+            w.newLine();
+        }
+
+        Path mvnRepo = Paths.get("target/test-classes/repo");
+        Builder builder = Builder.newInstance();
+        // defaultValue = "${project.build.directory}/assembly"
+        builder.homeDirectory(workDir);
+//                .repositories(Builder.Stage.Startup, true, "mvn:foo/baz/1.0/xml/features")
+
+//                .localRepository(mvnRepo.toString());
+        builder.generateAssembly();
+
+        // org.apache.karaf.tooling.AssemblyMojo.includeBuildOutputDirectory? -
+        // ${project.basedir}/src/main/resources/assembly - copied over assembly (no filtering - source files)
+    }
+
+    @Test
     public void testCyclicRepos() throws Exception {
         Path workDir = Paths.get("target/distrib");
         recursiveDelete(workDir);
diff --git a/profile/src/test/resources/log4j.properties b/profile/src/test/resources/log4j.properties
new file mode 100644
index 0000000..8c07c5c
--- /dev/null
+++ b/profile/src/test/resources/log4j.properties
@@ -0,0 +1,26 @@
+## ---------------------------------------------------------------------------
+## 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
+
+# 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} [%5p] {%t} %C (%F:%L) %m%n

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