blob: 01fba3eba8ca11815caa2bc4b4ce54fdc84191b3 [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.impl;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.felix.utils.properties.TypedProperties;
import org.apache.karaf.profile.PlaceholderResolver;
import org.apache.karaf.profile.Profile;
import org.apache.karaf.profile.ProfileBuilder;
import static org.apache.karaf.profile.impl.Utils.assertNotNull;
/**
* Static utilities to work with {@link Profile profiles}.
*/
public final class Profiles {
public static final String PROFILE_FOLDER_SUFFIX = ".profile";
/**
* Loads profiles from given directory path. A profile is represented as directory with <code>.profile</code>
* extension. Subdirectories constitute part of {@link Profile#getId} - directory separators are changed to
* <code>-</code>.
* For example, profile contained in directory <code>mq/broker/standalone.profile</code> will have
* id = <code>mq-broker-standalone</code>.
*/
public static Map<String, Profile> loadProfiles(final Path root) throws IOException {
final Map<String, Profile> profiles = new HashMap<>();
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
ProfileBuilder builder;
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
Path fileName = dir.getFileName();
if (fileName != null && (fileName.toString().endsWith(PROFILE_FOLDER_SUFFIX)
|| fileName.toString().endsWith(PROFILE_FOLDER_SUFFIX + "/"))) {
String profileId = root.relativize(dir).toString();
if (profileId.endsWith("/")) {
profileId = profileId.substring(0, profileId.length() - 1);
}
profileId = profileId.replace(root.getFileSystem().getSeparator(), "-");
profileId = profileId.substring(0, profileId.length() - PROFILE_FOLDER_SUFFIX.length());
builder = ProfileBuilder.Factory.create(profileId);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc != null) {
throw exc;
}
if (builder != null) {
Profile profile = builder.getProfile();
profiles.put(profile.getId(), profile);
builder = null;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (builder != null) {
String pid = file.getFileName().toString();
byte[] data = Files.readAllBytes(file);
builder.addFileConfiguration(pid, data);
}
return FileVisitResult.CONTINUE;
}
}
);
return profiles;
}
/**
* Deletes profile by given {@link Profile#getId()} from <code>root</code> path.
* @param root
* @param id
* @throws IOException
*/
public static void deleteProfile(Path root, String id) throws IOException {
Path path = root.resolve(id.replace("-", root.getFileSystem().getSeparator()) + PROFILE_FOLDER_SUFFIX);
if (Files.isDirectory(path)) {
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
}
/**
* Writes given {@link Profile} under a path specified as <code>root</code>. Directory name to store a profile is
* derived from {@link Profile#getId()}
* @param root
* @param profile
* @throws IOException
*/
public static void writeProfile(Path root, Profile profile) throws IOException {
Path path = root.resolve(profile.getId().replace("-", root.getFileSystem().getSeparator()) + PROFILE_FOLDER_SUFFIX);
Files.createDirectories(path);
for (Map.Entry<String, byte[]> entry : profile.getFileConfigurations().entrySet()) {
Files.write(path.resolve(entry.getKey()), entry.getValue(), StandardOpenOption.CREATE_NEW);
}
}
/**
* <p>Gets an <em>overlay</em> profile for given <code>profile</code>, where passed in map of additional profiles
* is searched for possible parent profiles of given <code>profile</code>.</p>
* @param profile
* @param profiles
* @return
*/
public static Profile getOverlay(Profile profile, Map<String, Profile> profiles) {
return getOverlay(profile, profiles, null);
}
/**
* <p>Gets an <em>overlay</em> profile for given <code>profile</code>, where passed in map of additional profiles
* is searched for possible parent profiles of given <code>profile</code>.</p>
* <p><code>environment</code> may be used to select different <em>variants</em> of profile configuration files.
* For example, if <code>environment</code> is specified, configuration for <code>my.pid</code> PID will be read
* from <code>my.pid.cfg#&lt;environment&gt;</code>.</p>
* @param profile
* @param profiles
* @param environment
* @return
*/
public static Profile getOverlay(Profile profile, Map<String, Profile> profiles, String environment) {
assertNotNull(profile, "profile is null");
assertNotNull(profile, "profiles is null");
if (profile.isOverlay()) {
return profile;
} else {
String profileId = profile.getId();
ProfileBuilder builder = ProfileBuilder.Factory.create(profileId);
new OverlayOptionsProvider(profiles, profile, environment).addOptions(builder);
return builder.getProfile();
}
}
/**
* Gets an <code>effective</code> profile with single property placeholder resolver for <code>${profile:xxx}</code>
* placeholders and with <code>finalSubstitution</code> set to <code>true</code>.
* @param profile
* @return
*/
public static Profile getEffective(final Profile profile) {
return getEffective(profile, true);
}
/**
* Gets an <code>effective</code> profile with single property placeholder resolver for <code>${profile:xxx}</code>
* placeholders.
* @param profile
* @param finalSubstitution
* @return
*/
public static Profile getEffective(final Profile profile, boolean finalSubstitution) {
return getEffective(profile,
Collections.singleton(new PlaceholderResolvers.ProfilePlaceholderResolver()),
finalSubstitution);
}
/**
* Gets an <code>effective</code> profile with <code>finalSubstitution</code> set to <code>true</code>.
* @param profile
* @param resolvers
* @return
*/
public static Profile getEffective(final Profile profile,
final Collection<PlaceholderResolver> resolvers) {
return getEffective(profile, resolvers, true);
}
/**
* <p>Gets an <em>effective</em> profile for given <code>profile</code>. Effective profile has all property
* placeholders resolved. When <code>finalSubstitution</code> is <code>true</code>, placeholders that can't
* be resolved are replaced with empty strings. When it's <code>false</code>, placeholders are left unchanged.</p>
* @param profile
* @param resolvers
* @param finalSubstitution
* @return
*/
public static Profile getEffective(final Profile profile,
final Collection<PlaceholderResolver> resolvers,
boolean finalSubstitution) {
assertNotNull(profile, "profile is null");
assertNotNull(profile, "resolvers is null");
final Map<String, TypedProperties> originals = new HashMap<>();
final Map<String, TypedProperties> originals2 = new HashMap<>();
for (Map.Entry<String, byte[]> entry : profile.getFileConfigurations().entrySet()) {
if (entry.getKey().endsWith(Profile.PROPERTIES_SUFFIX)) {
try {
String key = entry.getKey().substring(0, entry.getKey().length() - Profile.PROPERTIES_SUFFIX.length());
TypedProperties props = new TypedProperties(false);
props.load(new ByteArrayInputStream(entry.getValue()));
originals.put(key, props);
props = new TypedProperties(false);
props.load(new ByteArrayInputStream(entry.getValue()));
originals2.put(key, props);
} catch (IOException e) {
throw new IllegalArgumentException("Can not load properties for " + entry.getKey());
}
}
}
final Map<String, Map<String, String>> dynamic = TypedProperties.prepare(originals);
TypedProperties.substitute(originals, dynamic, (pid, key, value) -> {
if (value != null) {
for (PlaceholderResolver resolver : resolvers) {
if (resolver.getScheme() == null) {
String val = resolver.resolve(dynamic, pid, key, value);
if (val != null) {
return val;
}
}
}
if (value.contains(":")) {
String scheme = value.substring(0, value.indexOf(":"));
String toSubst = value.substring(scheme.length() + 1);
for (PlaceholderResolver resolver : resolvers) {
if (scheme.equals(resolver.getScheme())) {
String val = resolver.resolve(dynamic, pid, key, toSubst);
if (val != null) {
return val;
}
}
}
}
}
return null;
}, finalSubstitution);
// Force computation while preserving layout
ProfileBuilder builder = ProfileBuilder.Factory.createFrom(profile);
for (String cfg : originals.keySet()) {
TypedProperties original = originals.get(cfg);
TypedProperties original2 = originals2.get(cfg);
original2.putAll(original);
builder.addFileConfiguration(cfg + Profile.PROPERTIES_SUFFIX, Utils.toBytes(original2));
}
// Compute the new profile
return builder.getProfile();
}
/**
* <p>Helper internal class to configure {@link ProfileBuilder} used to create an <em>overlay</em> profile.</p>
* <p>There are strict rules built on a concept of profiles being <em>containers of file configurations</em>.
* Each profile may contain files with the same name. Profiles may be set in multi-parent - child relationship.
* Such graph of profiles is searched in depth-first fashion, while child (being a root of the graph) has
* highest priority.</p>
* <p>Files from higher-priority profile override files from parent profiles. Special case are PID files (with
* {@link Profile#PROPERTIES_SUFFIX} extension). These files are not simply taken from child profiles. Child
* profiles may have own version of given PID configuration file, but these files are overwritten at property
* level.</p>
* <p>For example, if parent profile specifies:<pre>
* property1 = v1
* property2 = v2
* </pre> and child profile specifies:<pre>
* property1 = v1a
* property3 = v3a
* </pre>an <em>overlay</em> profile for child profile uses:<pre>
* property1 = v1a
* property2 = v2
* property3 = v3a
* </pre></p>
*/
static private class OverlayOptionsProvider {
private final Map<String, Profile> profiles;
private final Profile self;
private final String environment;
private static class SupplementControl {
byte[] data;
TypedProperties props;
}
private OverlayOptionsProvider(Map<String, Profile> profiles, Profile self, String environment) {
this.profiles = profiles;
this.self = self;
this.environment = environment;
}
private ProfileBuilder addOptions(ProfileBuilder builder) {
builder.setAttributes(self.getAttributes());
builder.setFileConfigurations(getFileConfigurations());
builder.setOverlay(true);
return builder;
}
private Map<String, byte[]> getFileConfigurations() {
Map<String, SupplementControl> aggregate = new HashMap<>();
for (Profile profile : getInheritedProfiles()) {
supplement(profile, aggregate);
}
Map<String, byte[]> rc = new HashMap<>();
for (Map.Entry<String, SupplementControl> entry : aggregate.entrySet()) {
SupplementControl ctrl = entry.getValue();
if (ctrl.props != null) {
ctrl.data = Utils.toBytes(ctrl.props);
}
rc.put(entry.getKey(), ctrl.data);
}
return rc;
}
private List<Profile> getInheritedProfiles() {
List<Profile> profiles = new ArrayList<>();
fillParentProfiles(self, profiles);
return profiles;
}
private void fillParentProfiles(Profile profile, List<Profile> profiles) {
if (!profiles.contains(profile)) {
for (String parentId : profile.getParentIds()) {
Profile parent = getRequiredProfile(parentId);
fillParentProfiles(parent, profiles);
}
profiles.add(profile);
}
}
private void supplement(Profile profile, Map<String, SupplementControl> aggregate) {
Map<String, byte[]> configs = profile.getFileConfigurations();
for (String key : configs.keySet()) {
// Ignore environment specific configs
if (key.contains("#")) {
continue;
}
byte[] value = configs.get(key);
if (environment != null && configs.containsKey(key + "#" + environment)) {
value = configs.get(key + "#" + environment);
}
// we can use fine grained inheritance based updating if it's
// a properties file.
if (key.endsWith(Profile.PROPERTIES_SUFFIX)) {
SupplementControl ctrl = aggregate.get(key);
if (ctrl != null) {
// we can update the file..
TypedProperties childMap = Utils.toProperties(value);
if (childMap.remove(Profile.DELETED) != null) {
ctrl.props.clear();
}
// Update the entries...
for (Map.Entry<String, Object> p : childMap.entrySet()) {
if (Profile.DELETED.equals(p.getValue())) {
ctrl.props.remove(p.getKey());
} else {
ctrl.props.put(p.getKey(), p.getValue());
}
}
} else {
// new file..
ctrl = new SupplementControl();
ctrl.props = Utils.toProperties(value);
aggregate.put(key, ctrl);
}
} else {
// not a properties file? we can only overwrite.
SupplementControl ctrl = new SupplementControl();
ctrl.data = value;
aggregate.put(key, ctrl);
}
}
}
private Profile getRequiredProfile(String id) {
Profile profile = profiles.get(id);
if (profile == null) {
throw new IllegalStateException("Unable to find required profile " + id);
}
return profile;
}
}
private Profiles() { }
}