blob: a4d4205cf9c1639911517f8cc53d92a744932188 [file] [log] [blame]
/*
* 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.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("&", "&amp;").replaceAll(">", "&lt;").replaceAll("<", "&gt;").replaceAll("\"", "&quot;");
}
/**
* 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());
for (String location : new TreeSet<>(entry.getValue())) {
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");
}
}