blob: be327d1f61013371dc9b79254ad29dd4328a8ced [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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
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.InterpolationHelper;
import org.apache.felix.utils.properties.Properties;
import org.apache.felix.utils.version.VersionRange;
import org.apache.felix.utils.version.VersionTable;
import org.apache.karaf.features.FeaturesService;
import org.apache.karaf.features.Library;
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.download.impl.DownloadManagerHelper;
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.repository.BaseRepository;
import org.apache.karaf.features.internal.resolver.ResourceBuilder;
import org.apache.karaf.features.internal.service.Blacklist;
import org.apache.karaf.features.internal.service.Deployer;
import org.apache.karaf.features.internal.util.MapUtils;
import org.apache.karaf.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.KarafPropertyEdit;
import org.apache.karaf.tools.utils.model.KarafPropertyEdits;
import org.apache.karaf.profile.versioning.VersionUtils;
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.Version;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.resource.Resource;
import org.osgi.service.resolver.Resolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.util.jar.JarFile.MANIFEST_NAME;
import static org.apache.karaf.profile.assembly.Builder.Stage.Startup;
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";
public static final String ORG_OPS4J_PAX_URL_MVN_PID = "org.ops4j.pax.url.mvn";
public static enum Stage {
Startup, Boot, Installed
}
public static enum KarafVersion {
v24, v3x, v4x
}
public static enum BlacklistPolicy {
Discard,
Fail
}
static class RepositoryInfo {
Stage stage;
boolean addAll;
}
//
// 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> blacklistedProfiles = new ArrayList<>();
List<String> blacklistedFeatures = new ArrayList<>();
List<String> blacklistedBundles = new ArrayList<>();
BlacklistPolicy blacklistPolicy = BlacklistPolicy.Discard;
List<String> libraries = new ArrayList<>();
String javase = "1.7";
KarafVersion karafVersion = KarafVersion.v4x;
String environment = null;
boolean useReferenceUrls;
boolean ignoreDependencyFlag;
int defaultStartLevel = 50;
Path homeDirectory;
boolean offline;
String localRepository;
String mavenRepositories;
private ScheduledExecutorService executor;
private DownloadManager manager;
private Resolver resolver;
private Path etcDirectory;
private Path systemDirectory;
private Map<String, Profile> allProfiles;
private KarafPropertyEdits propertyEdits;
public static Builder newInstance() {
return new Builder();
}
public Builder defaultStage(Stage stage) {
this.defaultStage = stage;
return this;
}
public Builder defaultAddAll(boolean addAll) {
this.defaultAddAll = addAll;
return this;
}
public Builder profilesUris(String... profilesUri) {
Collections.addAll(this.profilesUris, profilesUri);
return this;
}
public Builder libraries(String... libraries) {
Collections.addAll(this.libraries, libraries);
return this;
}
public Builder kars(String... kars) {
return kars(defaultStage, defaultAddAll, kars);
}
public Builder kars(boolean addAll, String... kars) {
return kars(defaultStage, addAll, kars);
}
public Builder kars(Stage stage, boolean addAll, String... kars) {
for (String kar : kars) {
RepositoryInfo info = new RepositoryInfo();
info.stage = stage;
info.addAll = addAll;
this.kars.put(kar, info);
}
return this;
}
public Builder repositories(String... repositories) {
return repositories(defaultStage, defaultAddAll, repositories);
}
public Builder repositories(boolean addAll, String... repositories) {
return repositories(defaultStage, addAll, repositories);
}
public Builder repositories(Stage stage, boolean addAll, String... repositories) {
for (String repository : repositories) {
RepositoryInfo info = new RepositoryInfo();
info.stage = stage;
info.addAll = addAll;
this.repositories.put(repository, info);
}
return this;
}
public Builder features(String... features) {
return features(defaultStage, features);
}
public Builder features(Stage stage, String... features) {
for (String feature : features) {
this.features.put(feature, stage);
}
return this;
}
public Builder bundles(String... bundles) {
return bundles(defaultStage, bundles);
}
public Builder bundles(Stage stage, String... bundles) {
for (String bundle : bundles) {
this.bundles.put(bundle, stage);
}
return this;
}
public Builder profiles(String... profiles) {
return profiles(defaultStage, profiles);
}
public Builder profiles(Stage stage, String... profiles) {
for (String profile : profiles) {
this.profiles.put(profile, stage);
}
return this;
}
public Builder homeDirectory(Path homeDirectory) {
if (homeDirectory == null) {
throw new IllegalArgumentException("homeDirectory is null");
}
this.homeDirectory = homeDirectory;
return this;
}
public Builder javase(String javase) {
if (javase == null) {
throw new IllegalArgumentException("javase is null");
}
this.javase = javase;
return this;
}
public Builder environment(String environment) {
this.environment = environment;
return this;
}
public Builder useReferenceUrls() {
return useReferenceUrls(true);
}
public Builder useReferenceUrls(boolean useReferenceUrls) {
this.useReferenceUrls = useReferenceUrls;
return this;
}
public Builder karafVersion(KarafVersion karafVersion) {
this.karafVersion = karafVersion;
return this;
}
public Builder defaultStartLevel(int defaultStartLevel) {
this.defaultStartLevel = defaultStartLevel;
return this;
}
public Builder ignoreDependencyFlag() {
return ignoreDependencyFlag(true);
}
public Builder ignoreDependencyFlag(boolean ignoreDependencyFlag) {
this.ignoreDependencyFlag = ignoreDependencyFlag;
return this;
}
public Builder offline(boolean offline) {
this.offline = offline;
return this;
}
public Builder offline() {
return offline(true);
}
public Builder localRepository(String localRepository) {
this.localRepository = localRepository;
return this;
}
public Builder mavenRepositories(String mavenRepositories) {
this.mavenRepositories = mavenRepositories;
return this;
}
public Builder staticFramework() {
// TODO: load this from resources
return staticFramework("4.0.0-SNAPSHOT");
}
public Builder staticFramework(String version) {
String staticFeaturesKar = String.format(STATIC_FEATURES_KAR, version);
return this.defaultStage(Startup).useReferenceUrls().kars(Startup, true, staticFeaturesKar);
}
public Builder blacklistProfiles(Collection<String> profiles) {
this.blacklistedProfiles.addAll(profiles);
return this;
}
public Builder blacklistFeatures(Collection<String> features) {
this.blacklistedFeatures.addAll(features);
return this;
}
public Builder blacklistBundles(Collection<String> bundles) {
this.blacklistedBundles.addAll(bundles);
return this;
}
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;
}
public List<String> getBlacklistedProfiles() {
return blacklistedProfiles;
}
public List<String> getBlacklistedFeatures() {
return blacklistedFeatures;
}
public List<String> getBlacklistedBundles() {
return blacklistedBundles;
}
public BlacklistPolicy getBlacklistPolicy() {
return blacklistPolicy;
}
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 {
doGenerateAssembly();
} finally {
if (executor != null) {
executor.shutdownNow();
}
}
}
private void doGenerateAssembly() throws Exception {
systemDirectory = homeDirectory.resolve("system");
etcDirectory = homeDirectory.resolve("etc");
LOGGER.info("Generating karaf assembly: " + homeDirectory);
//
// Create download manager
//
Dictionary<String, String> props = new Hashtable<>();
if (offline) {
props.put(ORG_OPS4J_PAX_URL_MVN_PID + "offline", "true");
}
if (localRepository != null) {
props.put(Builder.ORG_OPS4J_PAX_URL_MVN_PID + ".localRepository", localRepository);
}
if (mavenRepositories != null) {
props.put(Builder.ORG_OPS4J_PAX_URL_MVN_PID + ".repositories", mavenRepositories);
}
MavenResolver resolver = MavenResolvers.createMavenResolver(props, ORG_OPS4J_PAX_URL_MVN_PID);
executor = Executors.newScheduledThreadPool(8);
manager = new CustomDownloadManager(resolver, executor);
this.resolver = new ResolverImpl(new Slf4jResolverLog(LOGGER));
//
// Unzip kars
//
LOGGER.info("Unzipping kars");
Map<String, RepositoryInfo> repositories = new LinkedHashMap<>(this.repositories);
Downloader downloader = manager.createDownloader();
for (String kar : kars.keySet()) {
downloader.download(kar, null);
}
downloader.await();
for (String karUri : kars.keySet()) {
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()) {
repositories.put(repositoryUri.toString(), info);
}
}
//
// Propagate feature installation from repositories
//
Map<String, Stage> features = new LinkedHashMap<>(this.features);
Map<String, Features> karRepositories = loadRepositories(manager, repositories.keySet(), false);
for (String repo : repositories.keySet()) {
RepositoryInfo info = repositories.get(repo);
if (info.addAll) {
for (Feature feature : karRepositories.get(repo).getFeature()) {
features.put(feature.getId(), info.stage);
}
}
}
//
// Load profiles
//
LOGGER.info("Loading profiles");
allProfiles = new HashMap<>();
for (String profilesUri : profilesUris) {
String uri = profilesUri;
if (uri.startsWith("jar:") && uri.contains("!/")) {
uri = uri.substring("jar:".length(), uri.indexOf("!/"));
}
if (!uri.startsWith("file:")) {
downloader = manager.createDownloader();
downloader.download(uri, null);
downloader.await();
StreamProvider provider = manager.getProviders().get(uri);
profilesUri = profilesUri.replace(uri, provider.getFile().toURI().toString());
}
URI profileURI = URI.create(profilesUri);
Path profilePath;
try {
profilePath = Paths.get(profileURI);
} catch (FileSystemNotFoundException e) {
// file system does not exist, try to create it
FileSystem fs = FileSystems.newFileSystem(profileURI, new HashMap<String, Object>(), Builder.class.getClassLoader());
profilePath = fs.provider().getPath(profileURI);
}
allProfiles.putAll(Profiles.loadProfiles(profilePath));
// Handle blacklisted profiles
if (!blacklistedProfiles.isEmpty()) {
if (blacklistPolicy == BlacklistPolicy.Discard) {
// Override blacklisted profiles with empty ones
for (String profile : blacklistedProfiles) {
allProfiles.put(profile, ProfileBuilder.Factory.create(profile).getProfile());
}
} else {
// Remove profiles completely
allProfiles.keySet().removeAll(blacklistedProfiles);
}
}
}
// Generate profiles
Profile startupProfile = generateProfile(Stage.Startup, profiles, repositories, features, bundles);
Profile bootProfile = generateProfile(Stage.Boot, profiles, repositories, features, bundles);
Profile installedProfile = generateProfile(Stage.Installed, profiles, repositories, features, bundles);
//
// Compute overall profile
//
Profile overallProfile = ProfileBuilder.Factory.create(UUID.randomUUID().toString())
.setParents(Arrays.asList(startupProfile.getId(), bootProfile.getId(), installedProfile.getId()))
.getProfile();
Profile overallOverlay = Profiles.getOverlay(overallProfile, allProfiles, environment);
Profile overallEffective = Profiles.getEffective(overallOverlay, false);
manager = new CustomDownloadManager(resolver, executor, overallEffective);
Hashtable<String, String> agentProps = new Hashtable<>(overallEffective.getConfiguration(ORG_OPS4J_PAX_URL_MVN_PID));
final Map<String, String> properties = new HashMap<>();
properties.put("karaf.default.repository", "system");
InterpolationHelper.performSubstitution(agentProps, new InterpolationHelper.SubstitutionCallback() {
@Override
public String getValue(String key) {
return properties.get(key);
}
}, false, false, true);
Map<String, List<KarafPropertyEdit>> editsByFile = new HashMap<>();
//
// Write config and 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
LOGGER.info("Downloading libraries");
downloader = manager.createDownloader();
downloadLibraries(downloader, configProperties, overallEffective.getLibraries());
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
//
for (Map.Entry<String, byte[]> config : overallEffective.getFileConfigurations().entrySet()) {
Path configFile = etcDirectory.resolve(config.getKey());
Files.createDirectories(configFile.getParent());
Files.write(configFile, config.getValue());
}
//
// Handle overrides
//
if (!overallEffective.getOverrides().isEmpty()) {
Path overrides = etcDirectory.resolve("overrides.properties");
List<String> lines = new ArrayList<>();
lines.add("#");
lines.add("# Generated by the karaf assembly builder");
lines.add("#");
lines.addAll(overallEffective.getOverrides());
Files.write(overrides, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
//
// Handle blacklist
//
if (!blacklistedFeatures.isEmpty() || !blacklistedBundles.isEmpty()) {
Path blacklist = etcDirectory.resolve("blacklisted.properties");
List<String> lines = new ArrayList<>();
lines.add("#");
lines.add("# Generated by the karaf assembly builder");
lines.add("#");
if (!blacklistedFeatures.isEmpty()) {
lines.add("");
lines.add("# Features");
lines.addAll(blacklistedFeatures);
}
if (!blacklistedBundles.isEmpty()) {
lines.add("");
lines.add("# Bundles");
lines.addAll(blacklistedBundles);
}
Files.write(blacklist, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
//
// Startup stage
//
Profile startupEffective = startupStage(startupProfile);
//
// Boot stage
//
Set<Feature> allBootFeatures = bootStage(bootProfile, startupEffective);
//
// Installed stage
//
installStage(installedProfile, allBootFeatures);
// 'improve' configuration files.
if (propertyEdits != null) {
KarafPropertiesEditor editor = new KarafPropertiesEditor();
editor.setInputEtc(etcDirectory.toFile())
.setOutputEtc(etcDirectory.toFile())
.setEdits(propertyEdits);
editor.run();
}
}
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);
}
}
protected void downloadLibraries(Downloader downloader, final Properties config, Collection<String> libraries) 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;
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, new DownloadCallback() {
@Override
public void downloaded(final StreamProvider provider) throws Exception {
synchronized (provider) {
Path input = provider.getFile().toPath();
String name = filename != null ? filename : input.getFileName().toString();
if (provider.getUrl().startsWith("mvn:")) {
Path libOutput = homeDirectory.resolve(path).resolve(name);
Files.copy(input, libOutput, StandardCopyOption.REPLACE_EXISTING);
// 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
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);
}
}
*/
}
} else {
Path output = homeDirectory.resolve(path).resolve(name);
Files.copy(input, output, StandardCopyOption.REPLACE_EXISTING);
}
}
boolean export = Boolean.parseBoolean(clause.getDirective(LIBRARY_CLAUSE_EXPORT));
boolean delegate = Boolean.parseBoolean(clause.getDirective(LIBRARY_CLAUSE_DELEGATE));
if (export || delegate) {
Map<String, String> headers = getHeaders(provider);
String packages = headers.get(Constants.EXPORT_PACKAGE);
if (packages != null) {
Clause[] clauses = org.apache.felix.utils.manifest.Parser.parseHeader(packages);
if (export) {
String val = config.getProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA);
for (Clause clause : clauses) {
val += "," + clause.toString();
}
config.setProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, val);
}
if (delegate) {
String val = config.getProperty(Constants.FRAMEWORK_BOOTDELEGATION);
for (Clause clause : clauses) {
val += "," + clause.getName();
}
config.setProperty(Constants.FRAMEWORK_BOOTDELEGATION, val);
}
}
}
}
});
}
}
private void installStage(Profile installedProfile, Set<Feature> allBootFeatures) throws Exception {
//
// Handle installed profiles
//
Profile installedOverlay = Profiles.getOverlay(installedProfile, allProfiles, environment);
Profile installedEffective = Profiles.getEffective(installedOverlay, false);
Downloader downloader = manager.createDownloader();
// Load startup repositories
Map<String, Features> installedRepositories = loadRepositories(manager, installedEffective.getRepositories(), true);
// Compute startup feature dependencies
Set<Feature> allInstalledFeatures = new LinkedHashSet<>();
for (Features repo : installedRepositories.values()) {
allInstalledFeatures.addAll(repo.getFeature());
}
Set<Feature> installedFeatures = new LinkedHashSet<>();
// Add boot features for search
allInstalledFeatures.addAll(allBootFeatures);
for (String feature : installedEffective.getFeatures()) {
addFeatures(allInstalledFeatures, feature, installedFeatures, true);
}
for (Feature feature : installedFeatures) {
for (Bundle bundle : feature.getBundle()) {
if (!ignoreDependencyFlag || !bundle.isDependency()) {
installArtifact(downloader, bundle.getLocation().trim());
}
}
// Install config files
for (ConfigFile configFile : feature.getConfigfile()) {
installArtifact(downloader, configFile.getLocation().trim());
}
for (Conditional cond : feature.getConditional()) {
for (Bundle bundle : cond.getBundle()) {
if (!ignoreDependencyFlag || !bundle.isDependency()) {
installArtifact(downloader, bundle.getLocation().trim());
}
}
}
}
for (String location : installedEffective.getBundles()) {
installArtifact(downloader, location);
}
downloader.await();
}
private Set<Feature> bootStage(Profile bootProfile, Profile startupEffective) throws Exception {
//
// Handle boot profiles
//
Profile bootOverlay = Profiles.getOverlay(bootProfile, allProfiles, environment);
Profile bootEffective = Profiles.getEffective(bootOverlay, false);
// Load startup repositories
Map<String, Features> bootRepositories = loadRepositories(manager, bootEffective.getRepositories(), true);
// Compute startup feature dependencies
Set<Feature> allBootFeatures = new LinkedHashSet<>();
for (Features repo : bootRepositories.values()) {
allBootFeatures.addAll(repo.getFeature());
}
// Generate a global feature
Map<String, Dependency> generatedDep = new LinkedHashMap<>();
Feature generated = new Feature();
generated.setName(UUID.randomUUID().toString());
// Add feature dependencies
for (String dependency : bootEffective.getFeatures()) {
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/");
Bundle bun = new Bundle();
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
Set<Feature> bootFeatures = new LinkedHashSet<>();
addFeatures(allBootFeatures, generated.getName(), bootFeatures, true);
for (Feature feature : bootFeatures) {
// the feature is a startup feature, updating startup.properties file
LOGGER.info("Feature " + feature.getName() + " is defined as a boot feature");
// add the feature in the system folder
Set<String> locations = new LinkedHashSet<>();
for (Bundle bundle : feature.getBundle()) {
if (!ignoreDependencyFlag || !bundle.isDependency()) {
locations.add(bundle.getLocation().trim());
}
}
for (Conditional cond : feature.getConditional()) {
for (Bundle bundle : cond.getBundle()) {
if (!ignoreDependencyFlag || !bundle.isDependency()) {
locations.add(bundle.getLocation().trim());
}
}
}
// 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:", Arrays.asList("wrap"));
prereqs.put("war:", Arrays.asList("war"));
for (String location : locations) {
installArtifact(downloader, location);
for (Map.Entry<String, List<String>> entry : prereqs.entrySet()) {
if (location.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);
}
}
}
}
// Install config files
for (ConfigFile configFile : feature.getConfigfile()) {
installArtifact(downloader, configFile.getLocation().trim());
}
for (Conditional cond : feature.getConditional()) {
for (ConfigFile configFile : cond.getConfigfile()) {
installArtifact(downloader, configFile.getLocation().trim());
}
}
// 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.home}/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 LinkedHashSet<>(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) throws Exception {
//
// Compute startup
//
Profile startupOverlay = Profiles.getOverlay(startupProfile, allProfiles, environment);
Profile startupEffective = Profiles.getEffective(startupOverlay, false);
// Load startup repositories
LOGGER.info("Loading repositories");
Map<String, Features> startupRepositories = loadRepositories(manager, startupEffective.getRepositories(), false);
//
// Resolve
//
LOGGER.info("Resolving features");
Map<String, Integer> bundles =
resolve(manager,
resolver,
startupRepositories.values(),
startupEffective.getFeatures(),
startupEffective.getBundles(),
startupEffective.getOverrides(),
startupEffective.getOptionals());
//
// 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 : invertedStartupBundles.entrySet()) {
String startLevel = Integer.toString(entry.getKey());
for (String location : new TreeSet<>(entry.getValue())) {
// remove any custom deployer prefix
int mvnIndex = location.indexOf("mvn:");
if (mvnIndex > 0) {
location = location.substring(mvnIndex);
}
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;
}
private void installArtifact(Downloader downloader, String location) throws Exception {
LOGGER.info("== Installing artifact " + location);
location = DownloadManagerHelper.stripUrl(location);
// remove any custom deployer prefix
int mvnIndex = location.indexOf("mvn:");
if (mvnIndex > 0) {
location = location.substring(mvnIndex);
}
if (location.startsWith("mvn:")) {
if (location.endsWith("/")) {
// for bad formed URL (like in Camel for mustache-compiler), we remove the trailing /
location = location.substring(0, location.length() - 1);
}
downloader.download(location, new DownloadCallback() {
@Override
public void downloaded(final StreamProvider provider) throws Exception {
String uri = provider.getUrl();
if (Blacklist.isBundleBlacklisted(blacklistedBundles, uri)) {
throw new RuntimeException("Bundle " + uri + " is blacklisted");
}
Path path = pathFromProviderUrl(uri);
synchronized (provider) {
Files.createDirectories(path.getParent());
Files.copy(provider.getFile().toPath(), path, StandardCopyOption.REPLACE_EXISTING);
}
}
});
} else {
LOGGER.warn("Ignoring artifact " + location);
}
}
private void addFeatures(Set<Feature> allFeatures, String feature, Set<Feature> features, boolean mandatory) {
String name;
VersionRange range;
int idx = feature.indexOf('/');
if (idx > 0) {
name = feature.substring(0, idx);
String version = feature.substring(idx + 1);
version = version.trim();
if (version.equals(org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION)) {
range = new VersionRange(Version.emptyVersion);
} else {
range = new VersionRange(version, true, true);
}
} else {
name = feature;
range = new VersionRange(Version.emptyVersion);
}
Set<Feature> set = new LinkedHashSet<>();
for (Feature f : allFeatures) {
if (f.getName().equals(name) && range.contains(VersionTable.getVersion(f.getVersion()))) {
set.add(f);
}
}
if (mandatory && set.isEmpty()) {
throw new IllegalStateException("Could not find matching feature for " + feature);
}
for (Feature f : set) {
features.add(f);
for (Dependency dep : f.getFeature()) {
addFeatures(allFeatures, dep.toString(), features, !dep.isDependency() && !dep.isPrerequisite());
}
}
}
private String getFeatureSt(Dependency dep) {
String version = dep.getVersion() == null || "0.0.0".equals(dep.getVersion()) ? "" : "/" + dep.getVersion();
return dep.getName() + version;
}
/**
* Checks if a given feature f matches the featureRef.
* TODO Need to also check for version ranges. Currently ranges are ignored and all features matching the name
* are copied in that case.
*
* @param f
* @param featureRef
* @return
*/
private boolean matches(Feature f, Dependency featureRef) {
if (!f.getName().equals(featureRef.getName())) {
return false;
}
final String featureRefVersion = featureRef.getVersion();
if (featureRefVersion == null) {
return true;
}
if (featureRefVersion.equals("0.0.0")) {
return true;
}
if (featureRefVersion.startsWith("[")) {
return true;
}
return VersionUtils.versionEquals(f.getVersion(), featureRefVersion);
}
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;
}
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) throws Exception {
final Map<String, Features> loaded = new HashMap<>();
final Downloader downloader = manager.createDownloader();
final List<String> blacklist = new ArrayList<>();
blacklist.addAll(blacklistedBundles);
blacklist.addAll(blacklistedFeatures);
final Clause[] clauses = org.apache.felix.utils.manifest.Parser.parseClauses(blacklist.toArray(new String[blacklist.size()]));
for (String repository : repositories) {
downloader.download(repository, new DownloadCallback() {
@Override
public void downloaded(final StreamProvider provider) throws Exception {
String url = provider.getUrl();
synchronized (loaded) {
if (!loaded.containsKey(provider.getUrl())) {
if (install) {
synchronized (provider) {
Path path = pathFromProviderUrl(url);
Files.createDirectories(path.getParent());
Files.copy(provider.getFile().toPath(), path, StandardCopyOption.REPLACE_EXISTING);
}
}
try (InputStream is = provider.open()) {
Features featuresModel = JaxbUtil.unmarshal(url, is, false);
if (blacklistPolicy == BlacklistPolicy.Discard) {
Blacklist.blacklist(featuresModel, clauses);
}
loaded.put(provider.getUrl(), featuresModel);
for (String innerRepository : featuresModel.getRepository()) {
downloader.download(innerRepository, this);
}
}
}
}
}
});
}
downloader.await();
return loaded;
}
private Profile generateProfile(Stage stage, Map<String, Stage> profiles, Map<String, RepositoryInfo> repositories, Map<String, Stage> features, Map<String, Stage> bundles) {
Profile profile = ProfileBuilder.Factory.create(UUID.randomUUID().toString())
.setParents(getStaged(stage, profiles))
.setRepositories(getStagedRepositories(stage, repositories))
.setFeatures(getStaged(stage, features))
.setBundles(getStaged(stage, bundles))
.getProfile();
allProfiles.put(profile.getId(), profile);
return profile;
}
private Map<String, Integer> resolve(
DownloadManager manager,
Resolver resolver,
Collection<Features> repositories,
Collection<String> features,
Collection<String> bundles,
Collection<String> overrides,
Collection<String> optionals) throws Exception {
BundleRevision systemBundle = getSystemBundle();
AssemblyDeployCallback callback = new AssemblyDeployCallback(manager, this, systemBundle, repositories);
Deployer deployer = new Deployer(manager, resolver, callback);
// Install framework
Deployer.DeploymentRequest request = createDeploymentRequest();
// Add overrides
request.overrides.addAll(overrides);
// Add optional resources
final List<Resource> resources = new ArrayList<>();
Downloader downloader = manager.createDownloader();
for (String optional : optionals) {
downloader.download(optional, new DownloadCallback() {
@Override
public void downloaded(StreamProvider provider) throws Exception {
Resource resource = ResourceBuilder.build(provider.getUrl(), getHeaders(provider));
synchronized (resources) {
resources.add(resource);
}
}
});
}
downloader.await();
request.globalRepository = new BaseRepository(resources);
// Install features
for (String feature : features) {
MapUtils.addToMapSet(request.requirements, FeaturesService.ROOT_REGION, feature);
}
for (String bundle : bundles) {
MapUtils.addToMapSet(request.requirements, FeaturesService.ROOT_REGION, "bundle:" + bundle);
}
Set<String> prereqs = new LinkedHashSet<>();
while (true) {
try {
deployer.deploy(callback.getDeploymentState(), request);
break;
} catch (Deployer.PartialDeploymentException e) {
if (!prereqs.containsAll(e.getMissing())) {
prereqs.addAll(e.getMissing());
} else {
throw new Exception("Deployment aborted due to loop in missing prerequisites: " + e.getMissing());
}
}
}
return callback.getStartupBundles();
}
private Deployer.DeploymentRequest createDeploymentRequest() {
Deployer.DeploymentRequest request = new Deployer.DeploymentRequest();
request.bundleUpdateRange = FeaturesService.DEFAULT_BUNDLE_UPDATE_RANGE;
request.featureResolutionRange = FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE;
request.serviceRequirements = FeaturesService.SERVICE_REQUIREMENTS_DEFAULT;
request.overrides = new LinkedHashSet<>();
request.requirements = new HashMap<>();
request.stateChanges = new HashMap<>();
request.options = EnumSet.noneOf(FeaturesService.Option.class);
return request;
}
private BundleRevision getSystemBundle() throws Exception {
Path configPropPath = etcDirectory.resolve("config.properties");
Properties configProps = PropertiesLoader.loadPropertiesOrFail(configPropPath.toFile());
configProps.put("java.specification.version", javase);
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 (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);
}
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");
}
private Path pathFromProviderUrl(String url) throws MalformedURLException {
String pathString;
if (url.startsWith("file:")) {
return Paths.get(URI.create(url));
}
else if (url.startsWith("mvn:")) {
pathString = Parser.pathFromMaven(url);
}
else {
pathString = url;
}
return systemDirectory.resolve(pathString);
}
}