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 fa8f79690961398e0194a5c0c3b1cb7be7e4d937 Author: Grzegorz Grzybek <[email protected]> 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#<environment></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:=<url>][;type:=<type>][;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 "[email protected]" <[email protected]>.
