| /* |
| * 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.assembly; |
| |
| import java.io.BufferedWriter; |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.MalformedURLException; |
| import java.net.URI; |
| import java.nio.file.FileSystem; |
| import java.nio.file.FileSystemNotFoundException; |
| import java.nio.file.FileSystems; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.StandardCopyOption; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Dictionary; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Hashtable; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| import java.util.UUID; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.ScheduledExecutorService; |
| import java.util.function.Function; |
| import java.util.jar.Attributes; |
| import java.util.jar.Manifest; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipInputStream; |
| import org.apache.felix.resolver.ResolverImpl; |
| import org.apache.felix.utils.manifest.Clause; |
| import org.apache.felix.utils.properties.Properties; |
| import org.apache.felix.utils.repository.BaseRepository; |
| import org.apache.felix.utils.resource.ResourceBuilder; |
| import org.apache.karaf.features.BundleInfo; |
| 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; |
| import org.apache.karaf.features.internal.download.StreamProvider; |
| import org.apache.karaf.features.internal.model.Bundle; |
| import org.apache.karaf.features.internal.model.Conditional; |
| import org.apache.karaf.features.internal.model.ConfigFile; |
| 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.service.Blacklist; |
| import org.apache.karaf.features.internal.service.Deployer; |
| import org.apache.karaf.features.internal.service.FeaturesProcessor; |
| import org.apache.karaf.features.internal.service.FeaturesProcessorImpl; |
| import org.apache.karaf.features.internal.service.Overrides; |
| import org.apache.karaf.features.internal.util.MapUtils; |
| import org.apache.karaf.features.internal.util.MultiException; |
| import org.apache.karaf.kar.internal.Kar; |
| 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; |
| import org.ops4j.pax.url.mvn.MavenResolvers; |
| import org.osgi.framework.Constants; |
| import org.osgi.framework.wiring.BundleRevision; |
| import org.osgi.resource.Resource; |
| import org.osgi.service.repository.Repository; |
| import org.osgi.service.resolver.Resolver; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static java.util.Collections.singletonList; |
| import static java.util.Comparator.comparing; |
| import static java.util.jar.JarFile.MANIFEST_NAME; |
| 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"; |
| |
| private static final Logger LOGGER = LoggerFactory.getLogger(Builder.class); |
| |
| private static final String FEATURES_REPOSITORIES = "featuresRepositories"; |
| private static final String FEATURES_BOOT = "featuresBoot"; |
| |
| private static final String LIBRARY_CLAUSE_TYPE = "type"; |
| private static final String LIBRARY_CLAUSE_EXPORT = "export"; |
| private static final String LIBRARY_CLAUSE_DELEGATE = "delegate"; |
| private static final String START_LEVEL = "start-level"; |
| |
| 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 { |
| /** |
| * 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", 1), |
| Java17("1.7", 2), |
| Java18("1.8", 3), |
| Java9("9", 4), |
| Java10("10", 5), |
| Java11("11", 6); |
| |
| private String version; |
| private int ordinal; |
| |
| JavaVersion(String version, int ordinal) { |
| this.version = version; |
| this.ordinal = ordinal; |
| } |
| |
| public static JavaVersion from(String version) { |
| for (JavaVersion value : values()) { |
| if (value.version.equals(version)) { |
| return value; |
| } |
| } |
| throw new IllegalArgumentException("Java version \"" + version + "\" is not supported"); |
| } |
| |
| public boolean supportsEndorsedAndExtLibraries() { |
| return this.ordinal < Java9.ordinal; |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| } |
| |
| /** |
| * Class similar to {@link FeaturePattern} but simplified for profile name matching |
| */ |
| private static class ProfileNamePattern { |
| private String name; |
| private Pattern namePattern; |
| |
| |
| public ProfileNamePattern(String profileName) { |
| if (profileName == null) { |
| throw new IllegalArgumentException("Profile name to match should not be null"); |
| } |
| name = profileName; |
| if (name.contains("*")) { |
| namePattern = LocationPattern.toRegExp(name); |
| } |
| } |
| |
| /** |
| * Returns <code>if this feature pattern</code> matches given feature/version |
| * @param profileName |
| * @return |
| */ |
| public boolean matches(String profileName) { |
| if (profileName == null) { |
| return false; |
| } |
| if (namePattern != null) { |
| return namePattern.matcher(profileName).matches(); |
| } else { |
| return name.equals(profileName); |
| } |
| } |
| } |
| |
| // |
| // Input parameters |
| // |
| |
| List<String> profilesUris = new ArrayList<>(); |
| boolean defaultAddAll = true; |
| Stage defaultStage = Stage.Startup; |
| Map<String, RepositoryInfo> kars = new LinkedHashMap<>(); |
| Map<String, Stage> profiles = new LinkedHashMap<>(); |
| Map<String, RepositoryInfo> repositories = new LinkedHashMap<>(); |
| Map<String, Stage> features = new LinkedHashMap<>(); |
| Map<String, Stage> bundles = new LinkedHashMap<>(); |
| List<String> blacklistedProfileNames = new ArrayList<>(); |
| List<String> blacklistedFeatureIdentifiers = new ArrayList<>(); |
| List<String> blacklistedBundleURIs = new ArrayList<>(); |
| List<String> blacklistedRepositoryURIs = new ArrayList<>(); |
| BlacklistPolicy blacklistPolicy = BlacklistPolicy.Discard; |
| List<String> libraries = new ArrayList<>(); |
| JavaVersion javase = JavaVersion.Java18; |
| KarafVersion karafVersion = KarafVersion.v4x; |
| String environment = null; |
| boolean useReferenceUrls; |
| boolean ignoreDependencyFlag; |
| int defaultStartLevel = 50; |
| Path homeDirectory; |
| Path featuresProcessingLocation; |
| boolean offline; |
| String localRepository; |
| String mavenRepositories; |
| Map<String, String> config = new LinkedHashMap<>(); |
| Map<String, String> system = new LinkedHashMap<>(); |
| List<String> pidsToExtract = new LinkedList<>(); |
| boolean writeProfiles; |
| String generateConsistencyReport; |
| String consistencyReportProjectName; |
| String consistencyReportProjectVersion; |
| |
| private ScheduledExecutorService executor; |
| private DownloadManager manager; |
| private Resolver resolver; |
| private Path etcDirectory; |
| private Path systemDirectory; |
| private Map<String, Profile> allProfiles; |
| private KarafPropertyEdits propertyEdits; |
| private FeaturesProcessing featuresProcessing = new FeaturesProcessing(); |
| private Map<String, String> translatedUrls; |
| private Blacklist blacklist; |
| private String generatedBootFeatureName; |
| |
| 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) { |
| 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) { |
| 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); |
| } |
| 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); |
| } |
| 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); |
| } |
| 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"); |
| } |
| this.homeDirectory = homeDirectory; |
| 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 = 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 builder to generate consistency report |
| * @param generateConsistencyReport |
| */ |
| public void generateConsistencyReport(String generateConsistencyReport) { |
| this.generateConsistencyReport = generateConsistencyReport; |
| } |
| |
| /** |
| * Configure project name to be used in consistency report |
| * @param consistencyReportProjectName |
| */ |
| public void setConsistencyReportProjectName(String consistencyReportProjectName) { |
| this.consistencyReportProjectName = consistencyReportProjectName; |
| } |
| |
| /** |
| * Configure project version to be used in consistency report |
| * @param consistencyReportProjectVersion |
| */ |
| public void setConsistencyReportProjectVersion(String consistencyReportProjectVersion) { |
| this.consistencyReportProjectVersion = consistencyReportProjectVersion; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * <p>Configures custom location for a file with features processing instructions. Normally this file is generated |
| * by the builder if any of blacklisted options are configured.</p> |
| * <p>If custom location is provided and it's not <code>etc/org.apache.karaf.features.xml</code>, it is copied</p> |
| * <p>If custom location is provided and it's <code>etc/org.apache.karaf.features.xml</code>, it's left as is</p> |
| * <p>Any additional blacklisting/overrides configuration via Maven configuration causes overwrite of original |
| * content.</p> |
| * @param featuresProcessing |
| */ |
| public Builder setFeaturesProcessing(Path featuresProcessing) { |
| this.featuresProcessingLocation = featuresProcessing; |
| return this; |
| } |
| |
| /** |
| * Ignore the dependency attribute (dependency="[true|false]") on bundles, effectively forcing their |
| * installation. |
| */ |
| 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; |
| } |
| |
| /** |
| * 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() { |
| 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.blacklistedProfileNames.addAll(profiles); |
| return this; |
| } |
| |
| /** |
| * Configure a list of blacklisted feature names (see {@link FeaturePattern}) |
| * @param features |
| * @return |
| */ |
| public Builder blacklistFeatures(Collection<String> features) { |
| this.blacklistedFeatureIdentifiers.addAll(features); |
| return this; |
| } |
| |
| /** |
| * Configure a list of blacklisted bundle URIs (see {@link LocationPattern}) |
| * @param bundles |
| * @return |
| */ |
| public Builder blacklistBundles(Collection<String> bundles) { |
| this.blacklistedBundleURIs.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.blacklistedRepositoryURIs.addAll(repositories); |
| return this; |
| } |
| |
| /** |
| * TODOCUMENT |
| * @param policy |
| * @return |
| */ |
| public Builder blacklistPolicy(BlacklistPolicy policy) { |
| this.blacklistPolicy = policy; |
| return this; |
| } |
| |
| /** |
| * Specify a set of edits to apply when moving etc files. |
| * @param propertyEdits the edits. |
| * @return this. |
| */ |
| public Builder propertyEdits(KarafPropertyEdits propertyEdits) { |
| this.propertyEdits = propertyEdits; |
| 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) { |
| this.pidsToExtract.add(pid.trim()); |
| } |
| } |
| return this; |
| } |
| |
| /** |
| * Specify a set of url mappings to use instead of |
| * downloading from the original urls. |
| * @param translatedUrls the urls translations. |
| * @return this. |
| */ |
| public Builder translatedUrls(Map<String, String> translatedUrls) { |
| this.translatedUrls = translatedUrls; |
| 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; |
| } |
| |
| public List<String> getBlacklistedProfileNames() { |
| return blacklistedProfileNames; |
| } |
| |
| public List<String> getBlacklistedFeatureIdentifiers() { |
| return blacklistedFeatureIdentifiers; |
| } |
| |
| public List<String> getBlacklistedBundleURIs() { |
| return blacklistedBundleURIs; |
| } |
| |
| public List<String> getBlacklistedRepositoryURIs() { |
| return blacklistedRepositoryURIs; |
| } |
| |
| public BlacklistPolicy getBlacklistPolicy() { |
| return blacklistPolicy; |
| } |
| |
| public List<String> getPidsToExtract() { |
| 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"); |
| } |
| if (homeDirectory == null) { |
| 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) { |
| executor.shutdownNow(); |
| } |
| } |
| } |
| |
| private void doGenerateAssembly() throws Exception { |
| LOGGER.info("Generating Karaf assembly: " + homeDirectory); |
| |
| // |
| // Create download manager - combination of pax-url-aether and a resolver wrapper that may |
| // alter the way pax-url-aether resolver works |
| // |
| MavenResolver resolver = createMavenResolver(); |
| manager = new CustomDownloadManager(resolver, executor, null, translatedUrls); |
| this.resolver = new ResolverImpl(new Slf4jResolverLog(LOGGER)); |
| |
| // |
| // Unzip KARs |
| // |
| LOGGER.info("Unzipping kars"); |
| 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); |
| } |
| } |
| |
| // |
| // Load profiles |
| // |
| 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 initial profile to collect overrides and blacklisting instructions |
| Profile initialProfile = ProfileBuilder.Factory.create("initial") |
| .setParents(new ArrayList<>(profiles.keySet())) |
| .getProfile(); |
| Profile initialOverlay = Profiles.getOverlay(initialProfile, allProfiles, environment); |
| Profile initialEffective = Profiles.getEffective(initialOverlay, false); |
| |
| // |
| // Handle blacklist - we'll use SINGLE instance of Blacklist for all further downloads |
| // |
| blacklist = processBlacklist(initialEffective); |
| |
| // |
| // Configure blacklisting and overriding features processor |
| // |
| |
| boolean needFeaturesProcessorFileCopy = false; |
| String existingProcessorDefinitionURI = null; |
| Path existingProcessorDefinition = etcDirectory.resolve("org.apache.karaf.features.xml"); |
| if (existingProcessorDefinition.toFile().isFile()) { |
| existingProcessorDefinitionURI = existingProcessorDefinition.toFile().toURI().toString(); |
| LOGGER.info("Found existing features processor configuration: {}", homeDirectory.relativize(existingProcessorDefinition)); |
| } |
| if (featuresProcessingLocation != null && featuresProcessingLocation.toFile().isFile() |
| && !featuresProcessingLocation.equals(existingProcessorDefinition)) { |
| if (existingProcessorDefinitionURI != null) { |
| LOGGER.warn("Explicitly configured {} will be used for features processor configuration.", homeDirectory.relativize(featuresProcessingLocation)); |
| } else { |
| LOGGER.info("Found features processor configuration: {}", homeDirectory.relativize(featuresProcessingLocation)); |
| } |
| existingProcessorDefinitionURI = featuresProcessingLocation.toFile().toURI().toString(); |
| // when there are no other (configured via Maven for example) processing instructions (e.g., blacklisting) |
| // we don't have to generate this file and may take original content |
| needFeaturesProcessorFileCopy = true; |
| } |
| |
| // now we can configure blacklisting features processor which may have already defined (in XML) |
| // configuration for bundle replacements or feature overrides. |
| FeaturesProcessorImpl processor = new FeaturesProcessorImpl(existingProcessorDefinitionURI, null, blacklist, new HashSet<>()); |
| |
| // add overrides from initialProfile |
| Set<String> overrides = processOverrides(initialEffective.getOverrides()); |
| processor.addOverrides(overrides); |
| |
| // |
| // Propagate feature installation from repositories |
| // |
| LOGGER.info("Loading repositories"); |
| Map<String, Features> karRepositories = loadRepositories(manager, repositories.keySet(), false, processor); |
| for (String repo : repositories.keySet()) { |
| RepositoryInfo info = repositories.get(repo); |
| if (info.addAll) { |
| LOGGER.info(" adding all non-blacklisted features from repository: " + repo + " (stage: " + info.stage + ")"); |
| for (Feature feature : karRepositories.get(repo).getFeature()) { |
| if (feature.isBlacklisted()) { |
| LOGGER.info(" feature {}/{} is blacklisted - skipping.", feature.getId(), feature.getVersion()); |
| } else { |
| features.put(feature.getId(), info.stage); |
| } |
| } |
| } |
| } |
| |
| // |
| // 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 "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 with all the parents configured and stage-agnostic blacklisting configuration added |
| blacklistedRepositoryURIs.forEach(builder::addBlacklistedRepository); |
| blacklistedFeatureIdentifiers.forEach(builder::addBlacklistedFeature); |
| blacklistedBundleURIs.forEach(builder::addBlacklistedBundle); |
| // final profilep |
| 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> 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(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()); |
| configProperties.save(); |
| |
| Path systemPropertiesPath = etcDirectory.resolve("system.properties"); |
| Properties systemProperties = new Properties(systemPropertiesPath.toFile()); |
| systemProperties.putAll(overallEffective.getSystem()); |
| systemProperties.save(); |
| |
| // |
| // Download libraries |
| // |
| // TODO: handle karaf 2.x and 3.x 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); |
| configProperties.save(); |
| |
| // |
| // Write all configuration files |
| // |
| LOGGER.info("Writing configurations"); |
| for (Map.Entry<String, byte[]> config : overallEffective.getFileConfigurations().entrySet()) { |
| Path configFile = etcDirectory.resolve(config.getKey()); |
| if (Files.exists(configFile)) { |
| LOGGER.info(" not changing existing config file: {}", homeDirectory.relativize(configFile)); |
| } else { |
| LOGGER.info(" adding config file: {}", homeDirectory.relativize(configFile)); |
| Files.createDirectories(configFile.getParent()); |
| Files.write(configFile, config.getValue()); |
| } |
| } |
| |
| if (processor.hasInstructions()) { |
| Path featuresProcessingXml = etcDirectory.resolve("org.apache.karaf.features.xml"); |
| if (hasOwnInstructions() || overrides.size() > 0) { |
| // just generate new etc/org.apache.karaf.features.xml file (with external config + builder config) |
| try (FileOutputStream fos = new FileOutputStream(featuresProcessingXml.toFile())) { |
| LOGGER.info("Generating features processor configuration: {}", homeDirectory.relativize(featuresProcessingXml)); |
| processor.writeInstructions(fos); |
| } |
| } else if (needFeaturesProcessorFileCopy) { |
| // we may simply copy configured features processor XML configuration |
| LOGGER.info("Copying features processor configuration: {} -> {}", homeDirectory.relativize(featuresProcessingLocation), homeDirectory.relativize(featuresProcessingXml)); |
| Files.copy(featuresProcessingLocation, featuresProcessingXml, StandardCopyOption.REPLACE_EXISTING); |
| } |
| } |
| |
| // |
| // Startup stage |
| // |
| Profile startupEffective = startupStage(startupProfile, processor); |
| |
| // |
| // Boot stage |
| // |
| Set<Feature> allBootFeatures = bootStage(bootProfile, startupEffective, processor); |
| |
| // |
| // Installed stage |
| // |
| Set<Feature> allInstalledFeatures = installStage(installedProfile, allBootFeatures, processor); |
| |
| // 'improve' configuration files. |
| if (propertyEdits != null) { |
| KarafPropertiesEditor editor = new KarafPropertiesEditor(); |
| editor.setInputEtc(etcDirectory.toFile()) |
| .setOutputEtc(etcDirectory.toFile()) |
| .setEdits(propertyEdits); |
| editor.run(); |
| } |
| |
| if (generateConsistencyReport != null) { |
| File directory = new File(generateConsistencyReport); |
| if (directory.isFile()) { |
| LOGGER.warn("Can't generate consistency report into {} - it's not a directory", generateConsistencyReport); |
| } else { |
| if (!directory.exists()) { |
| directory.mkdirs(); |
| } |
| if (directory.isDirectory()) { |
| LOGGER.info("Writing bundle report"); |
| generateConsistencyReport(karRepositories, allInstalledFeatures, installedProfile, new File(directory, "bundle-report.xml")); |
| Files.copy(getClass().getResourceAsStream("/bundle-report.xslt"), |
| directory.toPath().resolve("bundle-report.xslt"), |
| StandardCopyOption.REPLACE_EXISTING); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Produces human readable XML with <em>feature consistency report</em>. |
| * @param repositories |
| * @param allInstalledFeatures |
| * @param installedProfile |
| * @param result |
| */ |
| public void generateConsistencyReport(Map<String, Features> repositories, Set<Feature> allInstalledFeatures, Profile installedProfile, File result) { |
| Profile installedOverlay = Profiles.getOverlay(installedProfile, allProfiles, environment); |
| Profile installedEffective = Profiles.getEffective(installedOverlay, false); |
| |
| List<String> installFeatures = new ArrayList<>(); |
| installFeatures.add(generatedBootFeatureName); |
| installFeatures.addAll(installedEffective.getFeatures()); |
| |
| FeatureSelector selector = new FeatureSelector(allInstalledFeatures); |
| Set<Feature> effectiveInstalledFeatures = selector.getMatching(installFeatures); |
| |
| if (result == null) { |
| return; |
| } |
| try (BufferedWriter writer = new BufferedWriter(new FileWriter(result))) { |
| writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); |
| writer.write("<?xml-stylesheet type=\"text/xsl\" href=\"bundle-report.xslt\"?>\n"); |
| writer.write("<consistency-report xmlns=\"urn:apache:karaf:consistency:1.0\" project=\"" + consistencyReportProjectName + "\" version=\"" + consistencyReportProjectVersion + "\">\n"); |
| |
| ReportFlavor[] flavors = new ReportFlavor[] { |
| all, |
| notBlacklisted, |
| new ReportFlavor() { |
| @Override |
| public String name() { |
| return "installed"; |
| } |
| |
| @Override |
| public boolean include(Features repository) { |
| return !repository.isBlacklisted(); |
| } |
| |
| @Override |
| public boolean include(Feature feature) { |
| return !feature.isBlacklisted() |
| && effectiveInstalledFeatures.contains(feature); |
| } |
| |
| @Override |
| public boolean include(BundleInfo bundle) { |
| return !bundle.isBlacklisted(); |
| } |
| } |
| }; |
| |
| for (ReportFlavor flavor : flavors) { |
| writer.write("<report flavor=\"" + flavor.name() + "\">\n"); |
| |
| Map<String, String> featureId2repository = new HashMap<>(); |
| // list of feature IDs containing given bundle URIs |
| Map<String, Set<String>> bundle2featureId = new TreeMap<>(new URIAwareComparator()); |
| // map of groupId/artifactId to full URI list to detect "duplicates" |
| Map<String, List<String>> ga2uri = new TreeMap<>(); |
| Set<String> haveDuplicates = new HashSet<>(); |
| |
| // collect closure of bundles and features |
| repositories.forEach((name, features) -> { |
| if (flavor.include(features)) { |
| features.getFeature().forEach(feature -> { |
| if (flavor.include(feature)) { |
| featureId2repository.put(feature.getId(), name); |
| feature.getBundle().forEach(bundle -> { |
| // normal bundles of feature |
| if (flavor.include(bundle)) { |
| bundle2featureId.computeIfAbsent(bundle.getLocation().trim(), k -> new TreeSet<>()).add(feature.getId()); |
| } |
| }); |
| feature.getConditional().forEach(cond -> cond.asFeature().getBundles().forEach(bundle -> { |
| // conditional bundles of feature |
| if (flavor.include(bundle)) { |
| bundle2featureId.computeIfAbsent(bundle.getLocation().trim(), k -> new TreeSet<>()).add(feature.getId()); |
| } |
| })); |
| } |
| }); |
| } |
| }); |
| // collect bundle URIs - for now, only wrap:mvn: and mvn: are interesting |
| bundle2featureId.keySet().forEach(uri -> { |
| String originalUri = uri; |
| if (uri.startsWith("wrap:mvn:")) { |
| uri = uri.substring(5); |
| if (uri.indexOf(";") > 0) { |
| uri = uri.substring(0, uri.indexOf(";")); |
| } |
| if (uri.indexOf("$") > 0) { |
| uri = uri.substring(0, uri.indexOf("$")); |
| } |
| } |
| if (uri.startsWith("mvn:")) { |
| try { |
| LocationPattern pattern = new LocationPattern(uri); |
| String ga = String.format("%s/%s", pattern.getGroupId(), pattern.getArtifactId()); |
| ga2uri.computeIfAbsent(ga, k -> new LinkedList<>()).add(originalUri); |
| } catch (IllegalArgumentException ignored) { |
| /* |
| <!-- hibernate-validator-osgi-karaf-features-5.3.4.Final-features.xml --> |
| <feature name="hibernate-validator-paranamer" version="5.3.4.Final"> |
| <feature>hibernate-validator</feature> |
| <bundle>wrap:mvn:com.thoughtworks.paranamer:paranamer:2.8</bundle> |
| </feature> |
| */ |
| } |
| } |
| }); |
| ga2uri.values().forEach(l -> { |
| if (l.size() > 1) { |
| haveDuplicates.addAll(l); |
| } |
| }); |
| writer.write(" <duplicates>\n"); |
| ga2uri.forEach((key, uris) -> { |
| if (uris.size() > 1) { |
| try { |
| writer.write(String.format(" <duplicate ga=\"%s\">\n", key)); |
| for (String uri : uris) { |
| writer.write(String.format(" <bundle uri=\"%s\">\n", sanitize(uri))); |
| for (String fid : bundle2featureId.get(uri)) { |
| writer.write(String.format(" <feature repository=\"%s\">%s</feature>\n", featureId2repository.get(fid), fid)); |
| } |
| writer.write(" </bundle>\n"); |
| } |
| writer.write(" </duplicate>\n"); |
| } catch (IOException ignored) { |
| } |
| } |
| }); |
| writer.write(" </duplicates>\n"); |
| writer.write(" <bundles>\n"); |
| for (String uri : bundle2featureId.keySet()) { |
| writer.write(String.format(" <bundle uri=\"%s\" duplicate=\"%b\">\n", sanitize(uri), haveDuplicates.contains(uri))); |
| for (String fid : bundle2featureId.get(uri)) { |
| writer.write(String.format(" <feature>%s</feature>\n", fid)); |
| } |
| writer.write(" </bundle>\n"); |
| } |
| writer.write(" </bundles>\n"); |
| writer.write("</report>\n"); |
| } |
| writer.write("</consistency-report>\n"); |
| } catch (IOException e) { |
| throw new RuntimeException(e.getMessage(), e); |
| } |
| } |
| |
| private interface ReportFlavor { |
| String name(); |
| boolean include(Features repository); |
| boolean include(Feature feature); |
| boolean include(BundleInfo bundle); |
| } |
| |
| private ReportFlavor all = new ReportFlavor() { |
| @Override |
| public String name() { |
| return "all"; |
| } |
| |
| @Override |
| public boolean include(Features repository) { |
| return true; |
| } |
| |
| @Override |
| public boolean include(Feature feature) { |
| return true; |
| } |
| |
| @Override |
| public boolean include(BundleInfo bundle) { |
| return true; |
| } |
| }; |
| |
| private ReportFlavor notBlacklisted = new ReportFlavor() { |
| @Override |
| public String name() { |
| return "available"; |
| } |
| |
| @Override |
| public boolean include(Features repository) { |
| return !repository.isBlacklisted(); |
| } |
| |
| @Override |
| public boolean include(Feature feature) { |
| return !feature.isBlacklisted(); |
| } |
| |
| @Override |
| public boolean include(BundleInfo bundle) { |
| return !bundle.isBlacklisted(); |
| } |
| }; |
| |
| /** |
| * Sanitize before putting to XML |
| * @param uri |
| * @return |
| */ |
| public String sanitize(String uri) { |
| return uri.replaceAll("&", "&").replaceAll(">", "<").replaceAll("<", ">").replaceAll("\"", """); |
| } |
| |
| /** |
| * Similar to {@link FeaturesProcessorImpl#hasInstructions()}, we check if there are any builder configuration |
| * options for blacklisted repos/features/bundles or overwrites. |
| * @return |
| */ |
| private boolean hasOwnInstructions() { |
| int count = 0; |
| count += blacklistedRepositoryURIs.size(); |
| count += blacklistedFeatureIdentifiers.size(); |
| count += blacklistedBundleURIs.size(); |
| |
| return count > 0; |
| } |
| |
| /** |
| * Checks existing (etc/overrides.properties) and configured (in profiles) overrides definitions |
| * @param profileOverrides |
| * @return |
| */ |
| private Set<String> processOverrides(List<String> profileOverrides) { |
| Set<String> result = new LinkedHashSet<>(); |
| Path existingOverridesLocation = etcDirectory.resolve("overrides.properties"); |
| if (existingOverridesLocation.toFile().isFile()) { |
| LOGGER.warn("Found {} which is deprecated, please use new feature processor configuration.", homeDirectory.relativize(existingOverridesLocation)); |
| result.addAll(Overrides.loadOverrides(existingOverridesLocation.toFile().toURI().toString())); |
| } |
| result.addAll(profileOverrides); |
| |
| return result; |
| } |
| |
| /** |
| * Checks existing and configured blacklisting definitions |
| * @param initialProfile |
| * @return |
| * @throws IOException |
| */ |
| private Blacklist processBlacklist(Profile initialProfile) throws IOException { |
| Blacklist existingBlacklist = null; |
| Blacklist blacklist = new Blacklist(); |
| Path existingBLacklistedLocation = etcDirectory.resolve("blacklisted.properties"); |
| if (existingBLacklistedLocation.toFile().isFile()) { |
| LOGGER.warn("Found {} which is deprecated, please use new feature processor configuration.", homeDirectory.relativize(existingBLacklistedLocation)); |
| existingBlacklist = new Blacklist(Files.readAllLines(existingBLacklistedLocation)); |
| } |
| for (String br : blacklistedRepositoryURIs) { |
| // from Maven/Builder configuration |
| try { |
| blacklist.blacklistRepository(new LocationPattern(br)); |
| } catch (IllegalArgumentException e) { |
| LOGGER.warn("Blacklisted features XML repository URI is invalid: {}, ignoring", br); |
| } |
| } |
| for (LocationPattern br : initialProfile.getBlacklistedRepositories()) { |
| // from profile configuration |
| blacklist.blacklistRepository(br); |
| } |
| for (String bf : blacklistedFeatureIdentifiers) { |
| // from Maven/Builder configuration |
| blacklist.blacklistFeature(new FeaturePattern(bf)); |
| } |
| for (FeaturePattern bf : initialProfile.getBlacklistedFeatures()) { |
| // from profile configuration |
| blacklist.blacklistFeature(bf); |
| } |
| for (String bb : blacklistedBundleURIs) { |
| // from Maven/Builder configuration |
| try { |
| blacklist.blacklistBundle(new LocationPattern(bb)); |
| } catch (IllegalArgumentException e) { |
| LOGGER.warn("Blacklisted bundle URI is invalid: {}, ignoring", bb); |
| } |
| } |
| for (LocationPattern bb : initialProfile.getBlacklistedBundles()) { |
| // from profile configuration |
| blacklist.blacklistBundle(bb); |
| } |
| if (existingBlacklist != null) { |
| blacklist.merge(existingBlacklist); |
| } |
| |
| return blacklist; |
| } |
| |
| 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<>(); |
| Map<String, Profile> filteredProfiles = 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 |
| List<ProfileNamePattern> blacklistedProfilePatterns = blacklistedProfileNames.stream() |
| .map(ProfileNamePattern::new).collect(Collectors.toList()); |
| |
| for (String profileName : profiles.keySet()) { |
| boolean blacklisted = false; |
| for (ProfileNamePattern pattern : blacklistedProfilePatterns) { |
| if (pattern.matches(profileName)) { |
| LOGGER.info(" blacklisting profile {} from {}", profileName, profilePath); |
| // TODO review blacklist policy options |
| if (blacklistPolicy == BlacklistPolicy.Discard) { |
| // Override blacklisted profiles with empty one |
| filteredProfiles.put(profileName, ProfileBuilder.Factory.create(profileName).getProfile()); |
| } else { |
| // Remove profile completely |
| } |
| // no need to check other patterns |
| blacklisted = true; |
| break; |
| } |
| } |
| if (!blacklisted) { |
| filteredProfiles.put(profileName, profiles.get(profileName)); |
| } |
| } |
| } |
| |
| return filteredProfiles; |
| } |
| |
| private void reformatClauses(Properties config, String key) { |
| String val = config.getProperty(key); |
| if (val != null && !val.isEmpty()) { |
| List<String> comments = config.getComments(key); |
| Clause[] clauses = org.apache.felix.utils.manifest.Parser.parseHeader(val); |
| Set<String> strings = new LinkedHashSet<>(); |
| for (Clause clause : clauses) { |
| strings.add(clause.toString()); |
| } |
| List<String> lines = new ArrayList<>(); |
| lines.add(""); |
| int index = 0; |
| for (String string : strings) { |
| String s = " " + string; |
| if (index++ < strings.size() - 1) { |
| s += ", "; |
| } |
| lines.add(s); |
| } |
| config.put(key, comments, lines); |
| } |
| } |
| |
| void downloadLibraries(Downloader downloader, final Properties config, Collection<String> libraries, String indent) throws MalformedURLException { |
| Clause[] clauses = org.apache.felix.utils.manifest.Parser.parseClauses(libraries.toArray(new String[libraries.size()])); |
| for (final Clause clause : clauses) { |
| final String filename; |
| final String library; |
| if (clause.getDirective("url") != null) { |
| filename = clause.getName(); |
| library = clause.getDirective("url"); |
| } else { |
| filename = null; |
| library = clause.getName(); |
| } |
| final String type = clause.getDirective(LIBRARY_CLAUSE_TYPE) != null |
| ? clause.getDirective(LIBRARY_CLAUSE_TYPE) : Library.TYPE_DEFAULT; |
| if (!javase.supportsEndorsedAndExtLibraries() && (Library.TYPE_ENDORSED.equals(type) || Library.TYPE_EXTENSION.equals(type))) { |
| LOGGER.warn("Ignoring library " + library + " of type " + type + " which is only supported for Java 1.8."); |
| continue; |
| } |
| final String path; |
| switch (type) { |
| case Library.TYPE_ENDORSED: path = "lib/endorsed"; break; |
| case Library.TYPE_EXTENSION: path = "lib/ext"; break; |
| case Library.TYPE_BOOT: path = "lib/boot"; break; |
| default: path = "lib"; break; |
| } |
| downloader.download(library, provider -> { |
| synchronized (downloader) { |
| Path input = provider.getFile().toPath(); |
| String name = filename != null ? filename : input.getFileName().toString(); |
| Path libOutput = homeDirectory.resolve(path).resolve(name); |
| if (!libOutput.toFile().getParentFile().isDirectory()) { |
| libOutput.toFile().getParentFile().mkdirs(); |
| } |
| LOGGER.info("{} adding library: {}", indent, homeDirectory.relativize(libOutput)); |
| Files.copy(input, libOutput, StandardCopyOption.REPLACE_EXISTING); |
| if (provider.getUrl().startsWith("mvn:")) { |
| // copy boot library in system repository |
| if (type.equals(Library.TYPE_BOOT)) { |
| String mvnPath = Parser.pathFromMaven(provider.getUrl()); |
| Path sysOutput = systemDirectory.resolve(mvnPath); |
| Files.createDirectories(sysOutput.getParent()); |
| Files.copy(input, sysOutput, StandardCopyOption.REPLACE_EXISTING); |
| libOutput = homeDirectory.resolve(path).resolve(name); |
| // copy the file |
| LOGGER.info("{} adding maven library: {}", indent, provider.getUrl()); |
| Files.copy(input, libOutput, StandardCopyOption.REPLACE_EXISTING); |
| /* a symlink could be used instead |
| |
| if (Files.notExists(libOutput, LinkOption.NOFOLLOW_LINKS)) { |
| try { |
| Files.createSymbolicLink(libOutput, libOutput.getParent().relativize(sysOutput)); |
| } catch (FileSystemException e) { |
| Files.copy(input, libOutput, StandardCopyOption.REPLACE_EXISTING); |
| } |
| } |
| */ |
| } |
| } |
| } |
| boolean export = Boolean.parseBoolean(clause.getDirective(LIBRARY_CLAUSE_EXPORT)); |
| boolean delegate = Boolean.parseBoolean(clause.getDirective(LIBRARY_CLAUSE_DELEGATE)); |
| |
| if (export || delegate) { |
| synchronized (config) { |
| Map<String, String> headers = getHeaders(provider); |
| String packages = headers.get(Constants.EXPORT_PACKAGE); |
| if (packages != null) { |
| Clause[] clauses1 = org.apache.felix.utils.manifest.Parser.parseHeader(packages); |
| if (export) { |
| StringBuilder val = new StringBuilder(config.getProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA)); |
| for (Clause clause1 : clauses1) { |
| val.append(",").append(clause1.toString()); |
| } |
| config.setProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, val.toString()); |
| } |
| if (delegate) { |
| StringBuilder val = new StringBuilder(config.getProperty(Constants.FRAMEWORK_BOOTDELEGATION)); |
| for (Clause clause1 : clauses1) { |
| val.append(",").append(clause1.getName()); |
| } |
| config.setProperty(Constants.FRAMEWORK_BOOTDELEGATION, val.toString()); |
| } |
| } |
| } |
| } |
| }); |
| } |
| } |
| |
| private Set<Feature> installStage(Profile installedProfile, Set<Feature> allBootFeatures, FeaturesProcessor processor) throws Exception { |
| LOGGER.info("Install stage"); |
| // |
| // Handle installed profiles |
| // |
| Profile installedOverlay = Profiles.getOverlay(installedProfile, allProfiles, environment); |
| Profile installedEffective = Profiles.getEffective(installedOverlay, false); |
| |
| Downloader downloader = manager.createDownloader(); |
| |
| // Load startup repositories |
| LOGGER.info(" Loading installed repositories"); |
| Map<String, Features> installedRepositories = loadRepositories(manager, installedEffective.getRepositories(), true, processor); |
| // Compute startup feature dependencies |
| Set<Feature> allInstalledFeatures = new HashSet<>(); |
| for (Features repo : installedRepositories.values()) { |
| allInstalledFeatures.addAll(repo.getFeature()); |
| } |
| |
| // Add boot features for search |
| allInstalledFeatures.addAll(allBootFeatures); |
| FeatureSelector selector = new FeatureSelector(allInstalledFeatures); |
| Set<Feature> installedFeatures = selector.getMatching(installedEffective.getFeatures()); |
| ArtifactInstaller installer = new ArtifactInstaller(systemDirectory, downloader, blacklist); |
| for (Feature feature : installedFeatures) { |
| if (feature.isBlacklisted()) { |
| LOGGER.info(" Feature " + feature.getId() + " is blacklisted, ignoring"); |
| continue; |
| } |
| LOGGER.info(" Feature {} is defined as an installed feature", feature.getId()); |
| for (Bundle bundle : feature.getBundle()) { |
| if (!ignoreDependencyFlag || !bundle.isDependency()) { |
| installer.installArtifact(bundle); |
| } |
| } |
| // Install config files |
| for (ConfigFile configFile : feature.getConfigfile()) { |
| installer.installArtifact(configFile.getLocation().trim()); |
| } |
| for (Conditional cond : feature.getConditional()) { |
| if (cond.isBlacklisted()) { |
| LOGGER.info(" Conditionial " + cond.getConditionId() + " is blacklisted, ignoring"); |
| } |
| for (Bundle bundle : cond.getBundle()) { |
| if (!ignoreDependencyFlag || !bundle.isDependency()) { |
| installer.installArtifact(bundle); |
| } |
| } |
| } |
| } |
| for (String location : installedEffective.getBundles()) { |
| installer.installArtifact(location); |
| } |
| downloader.await(); |
| return allInstalledFeatures; |
| } |
| |
| private Set<Feature> bootStage(Profile bootProfile, Profile startupEffective, FeaturesProcessor processor) throws Exception { |
| LOGGER.info("Boot stage"); |
| // |
| // Handle boot profiles |
| // |
| Profile bootOverlay = Profiles.getOverlay(bootProfile, allProfiles, environment); |
| Profile bootEffective = Profiles.getEffective(bootOverlay, false); |
| // Load startup repositories |
| LOGGER.info(" Loading boot repositories"); |
| Map<String, Features> bootRepositories = loadRepositories(manager, bootEffective.getRepositories(), true, processor); |
| // Compute startup feature dependencies |
| Set<Feature> allBootFeatures = new HashSet<>(); |
| for (Features repo : bootRepositories.values()) { |
| allBootFeatures.addAll(repo.getFeature()); |
| } |
| // Generate a global feature |
| Map<String, Dependency> generatedDep = new HashMap<>(); |
| generatedBootFeatureName = UUID.randomUUID().toString(); |
| Feature generated = new Feature(); |
| generated.setName(generatedBootFeatureName); |
| // Add feature dependencies |
| for (String nameOrPattern : bootEffective.getFeatures()) { |
| // KARAF-5273: feature may be a pattern |
| for (String dependency : FeatureSelector.getMatchingFeatures(nameOrPattern, bootRepositories.values())) { |
| Dependency dep = generatedDep.get(dependency); |
| if (dep == null) { |
| dep = createDependency(dependency); |
| generated.getFeature().add(dep); |
| generatedDep.put(dep.getName(), dep); |
| } |
| dep.setDependency(false); |
| } |
| } |
| // Add bundles |
| for (String location : bootEffective.getBundles()) { |
| location = location.replace("profile:", "file:etc/"); |
| int intLevel = -100; |
| if (location.contains(START_LEVEL)) { |
| //extract start-level for this bundle |
| String level = location.substring(location.indexOf(START_LEVEL)); |
| level = level.substring(START_LEVEL.length() + 1); |
| if (level.startsWith("\"")) { |
| level = level.substring(1, level.length() - 1); |
| } |
| intLevel = Integer.parseInt(level); |
| LOGGER.debug("bundle start-level: " + level); |
| location = location.substring(0, location.indexOf(START_LEVEL) - 1); |
| LOGGER.debug("new bundle location after strip start-level: " + location); |
| } |
| Bundle bun = new Bundle(); |
| if (intLevel > 0) { |
| bun.setStartLevel(intLevel); |
| } |
| bun.setLocation(location); |
| generated.getBundle().add(bun); |
| } |
| Features rep = new Features(); |
| rep.setName(UUID.randomUUID().toString()); |
| rep.getRepository().addAll(bootEffective.getRepositories()); |
| rep.getFeature().add(generated); |
| allBootFeatures.add(generated); |
| |
| Downloader downloader = manager.createDownloader(); |
| |
| // Compute startup feature dependencies |
| FeatureSelector selector = new FeatureSelector(allBootFeatures); |
| Set<Feature> bootFeatures = selector.getMatching(singletonList(generated.getName())); |
| for (Feature feature : bootFeatures) { |
| if (feature.isBlacklisted()) { |
| LOGGER.info(" Feature " + feature.getId() + " is blacklisted, ignoring"); |
| continue; |
| } |
| LOGGER.info(" Feature " + feature.getId() + " is defined as a boot feature"); |
| // add the feature in the system folder |
| Set<BundleInfo> bundleInfos = new HashSet<>(); |
| for (Bundle bundle : feature.getBundle()) { |
| if (!ignoreDependencyFlag || !bundle.isDependency()) { |
| bundleInfos.add(bundle); |
| } |
| } |
| for (Conditional cond : feature.getConditional()) { |
| if (cond.isBlacklisted()) { |
| LOGGER.info(" Conditionial " + cond.getConditionId() + " is blacklisted, ignoring"); |
| } |
| for (Bundle bundle : cond.getBundle()) { |
| if (!ignoreDependencyFlag || !bundle.isDependency()) { |
| bundleInfos.add(bundle); |
| } |
| } |
| } |
| |
| // Build optional features and known prerequisites |
| 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:", Collections.singletonList("wrap")); |
| prereqs.put("war:", Collections.singletonList("war")); |
| ArtifactInstaller installer = new ArtifactInstaller(systemDirectory, downloader, blacklist); |
| for (BundleInfo bundleInfo : bundleInfos) { |
| installer.installArtifact(bundleInfo); |
| for (Map.Entry<String, List<String>> entry : prereqs.entrySet()) { |
| if (bundleInfo.getLocation().trim().startsWith(entry.getKey())) { |
| for (String prereq : entry.getValue()) { |
| Dependency dep = generatedDep.get(prereq); |
| if (dep == null) { |
| dep = new Dependency(); |
| dep.setName(prereq); |
| generated.getFeature().add(dep); |
| generatedDep.put(dep.getName(), dep); |
| } |
| dep.setPrerequisite(true); |
| } |
| } |
| } |
| } |
| |
| new ConfigInstaller(etcDirectory, pidsToExtract) |
| .installConfigs(feature, downloader, installer); |
| // Install libraries |
| List<String> libraries = new ArrayList<>(); |
| for (Library library : feature.getLibraries()) { |
| String lib = library.getLocation() + |
| ";type:=" + library.getType() + |
| ";export:=" + library.isExport() + |
| ";delegate:=" + library.isDelegate(); |
| libraries.add(lib); |
| } |
| Path configPropertiesPath = etcDirectory.resolve("config.properties"); |
| Properties configProperties = new Properties(configPropertiesPath.toFile()); |
| downloadLibraries(downloader, configProperties, libraries, " "); |
| downloader.await(); |
| // Reformat clauses |
| reformatClauses(configProperties, Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA); |
| reformatClauses(configProperties, Constants.FRAMEWORK_BOOTDELEGATION); |
| configProperties.save(); |
| } |
| |
| // If there are bundles to install, we can't use the boot features only |
| // so keep the generated feature |
| Path featuresCfgFile = etcDirectory.resolve("org.apache.karaf.features.cfg"); |
| if (!generated.getBundle().isEmpty()) { |
| File output = etcDirectory.resolve(rep.getName() + ".xml").toFile(); |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| JaxbUtil.marshal(rep, baos); |
| ByteArrayInputStream bais; |
| String repoUrl; |
| if (karafVersion == KarafVersion.v24) { |
| String str = baos.toString(); |
| str = str.replace("http://karaf.apache.org/xmlns/features/v1.3.0", "http://karaf.apache.org/xmlns/features/v1.2.0"); |
| str = str.replaceAll(" dependency=\".*?\"", ""); |
| str = str.replaceAll(" prerequisite=\".*?\"", ""); |
| for (Feature f : rep.getFeature()) { |
| for (Dependency d : f.getFeature()) { |
| if (d.isPrerequisite()) { |
| if (!startupEffective.getFeatures().contains(d.getName())) { |
| LOGGER.warn("Feature " + d.getName() + " is a prerequisite and should be installed as a startup feature."); } |
| } |
| } |
| } |
| bais = new ByteArrayInputStream(str.getBytes()); |
| repoUrl = "file:etc/" + output.getName(); |
| } else { |
| bais = new ByteArrayInputStream(baos.toByteArray()); |
| repoUrl = "file:${karaf.etc}/" + output.getName(); |
| } |
| Files.copy(bais, output.toPath()); |
| Properties featuresProperties = new Properties(featuresCfgFile.toFile()); |
| featuresProperties.put(FEATURES_REPOSITORIES, repoUrl); |
| featuresProperties.put(FEATURES_BOOT, generated.getName()); |
| featuresProperties.save(); |
| } |
| else { |
| String repos = getRepos(rep); |
| String boot = getBootFeatures(generatedDep); |
| |
| Properties featuresProperties = new Properties(featuresCfgFile.toFile()); |
| featuresProperties.put(FEATURES_REPOSITORIES, repos); |
| featuresProperties.put(FEATURES_BOOT, boot); |
| reformatClauses(featuresProperties, FEATURES_REPOSITORIES); |
| reformatClauses(featuresProperties, FEATURES_BOOT); |
| featuresProperties.save(); |
| } |
| downloader.await(); |
| return allBootFeatures; |
| } |
| |
| private String getRepos(Features rep) { |
| StringBuilder repos = new StringBuilder(); |
| for (String repo : new HashSet<>(rep.getRepository())) { |
| if (repos.length() > 0) { |
| repos.append(","); |
| } |
| repos.append(repo); |
| } |
| return repos.toString(); |
| } |
| |
| private String getBootFeatures(Map<String, Dependency> generatedDep) { |
| StringBuilder boot = new StringBuilder(); |
| for (Dependency dep : generatedDep.values()) { |
| if (dep.isPrerequisite()) { |
| if (boot.length() == 0) { |
| boot.append("("); |
| } else { |
| boot.append(","); |
| } |
| boot.append(dep.getName()); |
| } |
| } |
| if (boot.length() > 0) { |
| boot.append(")"); |
| } |
| // TODO: for dependencies, we'd need to resolve the features completely |
| for (Dependency dep : generatedDep.values()) { |
| if (!dep.isPrerequisite() && !dep.isDependency()) { |
| if (boot.length() > 0) { |
| boot.append(","); |
| } |
| boot.append(dep.getName()); |
| if (!Feature.DEFAULT_VERSION.equals(dep.getVersion())) { |
| if (karafVersion == KarafVersion.v4x) { |
| boot.append("/"); |
| } else { |
| boot.append(";version="); |
| } |
| boot.append(dep.getVersion()); |
| } |
| } |
| } |
| return boot.toString(); |
| } |
| |
| private Dependency createDependency(String dependency) { |
| Dependency dep; |
| dep = new Dependency(); |
| String[] split = dependency.split("/"); |
| dep.setName(split[0]); |
| if (split.length > 1) { |
| dep.setVersion(split[1]); |
| } |
| return dep; |
| } |
| |
| private Profile startupStage(Profile startupProfile, FeaturesProcessor processor) throws Exception { |
| LOGGER.info("Startup stage"); |
| // |
| // Compute startup |
| // |
| Profile startupOverlay = Profiles.getOverlay(startupProfile, allProfiles, environment); |
| Profile startupEffective = Profiles.getEffective(startupOverlay, false); |
| // Load startup repositories |
| LOGGER.info(" Loading startup repositories"); |
| Map<String, Features> startupRepositories = loadRepositories(manager, startupEffective.getRepositories(), false, processor); |
| |
| // |
| // Resolve |
| // |
| LOGGER.info(" Resolving startup features and bundles"); |
| LOGGER.info(" Features: " + startupEffective.getFeatures().stream().collect(Collectors.joining(", "))); |
| LOGGER.info(" Bundles: " + startupEffective.getBundles().stream().collect(Collectors.joining(", "))); |
| |
| Map<String, Integer> bundles = |
| resolve(manager, |
| resolver, |
| startupRepositories.values(), |
| startupEffective.getFeatures(), |
| startupEffective.getBundles(), |
| startupEffective.getOptionals(), |
| processor); |
| |
| // |
| // Generate startup.properties |
| // |
| Properties startup = new Properties(); |
| startup.setHeader(Collections.singletonList("# Bundles to be started on startup, with startlevel")); |
| Map<Integer, Set<String>> invertedStartupBundles = MapUtils.invert(bundles); |
| for (Map.Entry<Integer, Set<String>> entry : new TreeMap<>(invertedStartupBundles).entrySet()) { |
| String startLevel = Integer.toString(entry.getKey()); |
| // ensure input order is respected whatever hashmap/set was in the middle of the processing |
| final List<String> value = new ArrayList<>(entry.getValue()); |
| value.sort(comparing(bnd -> startupEffective.getBundles().indexOf(bnd))); |
| for (String location : value) { |
| if (useReferenceUrls) { |
| if (location.startsWith("mvn:")) { |
| location = "file:" + Parser.pathFromMaven(location); |
| } |
| if (location.startsWith("file:")) { |
| location = "reference:" + location; |
| } |
| } |
| if (location.startsWith("file:") && karafVersion == KarafVersion.v24) { |
| location = location.substring("file:".length()); |
| } |
| startup.put(location, startLevel); |
| } |
| } |
| Path startupProperties = etcDirectory.resolve("startup.properties"); |
| startup.save(startupProperties.toFile()); |
| 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()) { |
| if (data.get(s) == stage) { |
| staged.add(s); |
| } |
| } |
| 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()) { |
| if (data.get(s).stage == stage || |
| data.get(s).stage == Stage.Startup && stage == Stage.Boot) { |
| // For boot stage, we also want the startup repositories |
| staged.add(s); |
| } |
| } |
| return staged; |
| } |
| |
| private Map<String, Features> loadRepositories(DownloadManager manager, Collection<String> repositories, final boolean install, FeaturesProcessor processor) throws Exception { |
| final Map<String, Features> loaded = new HashMap<>(); |
| final Downloader downloader = manager.createDownloader(); |
| for (String repository : repositories) { |
| downloader.download(repository, new DownloadCallback() { |
| @Override |
| public void downloaded(final StreamProvider provider) throws Exception { |
| String url = provider.getUrl(); |
| if (processor.isRepositoryBlacklisted(url)) { |
| LOGGER.info(" feature repository " + url + " is blacklisted"); |
| return; |
| } |
| synchronized (loaded) { |
| if (!loaded.containsKey(provider.getUrl())) { |
| if (install) { |
| synchronized (provider) { |
| Path path = ArtifactInstaller.pathFromProviderUrl(systemDirectory, url); |
| Files.createDirectories(path.getParent()); |
| LOGGER.info(" adding feature repository: " + url); |
| Files.copy(provider.getFile().toPath(), path, StandardCopyOption.REPLACE_EXISTING); |
| } |
| } |
| try (InputStream is = provider.open()) { |
| Features featuresModel = JaxbUtil.unmarshal(url, is, false); |
| // always process according to processor configuration |
| featuresModel.setBlacklisted(processor.isRepositoryBlacklisted(url)); |
| processor.process(featuresModel); |
| |
| loaded.put(provider.getUrl(), featuresModel); |
| for (String innerRepository : featuresModel.getRepository()) { |
| if (processor.isRepositoryBlacklisted(innerRepository)) { |
| LOGGER.info(" referenced feature repository " + innerRepository + " is blacklisted"); |
| continue; |
| } |
| downloader.download(innerRepository, this); |
| } |
| } |
| } |
| } |
| } |
| }); |
| } |
| downloader.await(); |
| return loaded; |
| } |
| |
| /** |
| * 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(); |
| } |
| |
| /** |
| * <p>Resolves set of features and bundles using OSGi resolver to calculate startup stage bundles.</p> |
| * <p>Startup stage means that <em>current</em> state of the OSGi framework is just single system bundle installed |
| * and bundles+features are being resolved against this single <em>bundle 0</em>.</p> |
| * |
| * @param manager {@link DownloadManager} to help downloading bundles and resources |
| * @param resolver OSGi resolver which will resolve features and bundles in framework with only system bundle installed |
| * @param repositories all available (not only to-be-installed) features |
| * @param features feature identifiers to resolve |
| * @param bundles bundle locations to resolve |
| * @param optionals optional URI locations that'll be available through {@link org.osgi.service.repository.Repository}, |
| * used in resolution process |
| * @param processor {@link FeaturesProcessor} to process repositories/features/bundles |
| * @return map from bundle URI to bundle start-level |
| * @throws Exception |
| */ |
| private Map<String, Integer> resolve( |
| DownloadManager manager, |
| Resolver resolver, |
| Collection<Features> repositories, |
| Collection<String> features, |
| Collection<String> bundles, |
| Collection<String> optionals, |
| FeaturesProcessor processor) throws Exception { |
| |
| // System bundle will be single bundle installed with bundleId == 0 |
| BundleRevision systemBundle = getSystemBundle(); |
| // Static distribution building callback and deployer that's used to deploy/collect startup-stage artifacts |
| AssemblyDeployCallback callback = new AssemblyDeployCallback(manager, this, systemBundle, repositories, processor); |
| Deployer deployer = new Deployer(manager, resolver, callback); |
| |
| // Install framework |
| Deployer.DeploymentRequest request = Deployer.DeploymentRequest.defaultDeploymentRequest(); |
| |
| // Add optional resources available through OSGi resource repository |
| request.globalRepository = repositoryOfOptionalResources(manager, optionals); |
| |
| // Specify feature requirements |
| for (String feature : features) { |
| // KARAF-5273: feature may be a pattern |
| for (String featureName : FeatureSelector.getMatchingFeatures(feature, repositories)) { |
| MapUtils.addToMapSet(request.requirements, FeaturesService.ROOT_REGION, featureName); |
| } |
| } |
| // Specify bundle requirements |
| for (String bundle : bundles) { |
| MapUtils.addToMapSet(request.requirements, FeaturesService.ROOT_REGION, "bundle:" + bundle); |
| } |
| |
| deployer.deployFully(callback.getDeploymentState(), request); |
| |
| return callback.getStartupBundles(); |
| } |
| |
| /** |
| * Optional resource URIs will be made available through OSGi {@link Repository} |
| * @param manager |
| * @param optionals |
| * @return |
| * @throws Exception |
| */ |
| private Repository repositoryOfOptionalResources(DownloadManager manager, Collection<String> optionals) |
| throws Exception { |
| final List<Resource> resources = new ArrayList<>(); |
| Downloader downloader = manager.createDownloader(); |
| for (String optional : optionals) { |
| downloader.download(optional, provider -> { |
| Resource resource = ResourceBuilder.build(provider.getUrl(), getHeaders(provider)); |
| synchronized (resources) { |
| resources.add(resource); |
| } |
| }); |
| } |
| downloader.await(); |
| return new BaseRepository(resources); |
| } |
| |
| /** |
| * Prepares {@link BundleRevision} that represents System Bundle (a.k.a. <em>bundle 0</em>) |
| * @return |
| * @throws Exception |
| */ |
| @SuppressWarnings("rawtypes") |
| private BundleRevision getSystemBundle() throws Exception { |
| Path configPropPath = etcDirectory.resolve("config.properties"); |
| Properties configProps = PropertiesLoader.loadPropertiesOrFail(configPropPath.toFile()); |
| configProps.put("java.specification.version", javase.version); |
| configProps.substitute(); |
| |
| Attributes attributes = new Attributes(); |
| attributes.putValue(Constants.BUNDLE_MANIFESTVERSION, "2"); |
| attributes.putValue(Constants.BUNDLE_SYMBOLICNAME, "system.bundle"); |
| attributes.putValue(Constants.BUNDLE_VERSION, "0.0.0"); |
| |
| 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, ""); |
| attributes.putValue(Constants.PROVIDE_CAPABILITY, systemCaps); |
| |
| final Hashtable<String, String> headers = new Hashtable<>(); |
| for (Map.Entry attr : attributes.entrySet()) { |
| headers.put(attr.getKey().toString(), attr.getValue().toString()); |
| } |
| |
| return new FakeBundleRevision(headers, "system-bundle", 0L); |
| } |
| |
| @SuppressWarnings("rawtypes") |
| private Map<String, String> getHeaders(StreamProvider provider) throws IOException { |
| try ( |
| ZipInputStream zis = new ZipInputStream(provider.open()) |
| ) { |
| ZipEntry entry; |
| while ((entry = zis.getNextEntry()) != null) { |
| if (MANIFEST_NAME.equals(entry.getName())) { |
| Attributes attributes = new Manifest(zis).getMainAttributes(); |
| Map<String, String> headers = new HashMap<>(); |
| for (Map.Entry attr : attributes.entrySet()) { |
| headers.put(attr.getKey().toString(), attr.getValue().toString()); |
| } |
| return headers; |
| } |
| } |
| } |
| throw new IllegalArgumentException("Resource " + provider.getUrl() + " does not contain a manifest"); |
| } |
| |
| } |