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:59 UTC

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

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>.