http://git-wip-us.apache.org/repos/asf/karaf/blob/90cb480d/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java ---------------------------------------------------------------------- diff --git a/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java new file mode 100644 index 0000000..34d4d0d --- /dev/null +++ b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileBuilderImpl.java @@ -0,0 +1,305 @@ +/* + * 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.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.karaf.profile.Profile; +import org.apache.karaf.profile.ProfileBuilder; + +import static org.apache.karaf.profile.impl.ProfileImpl.ConfigListType; + +/** + * The default {@link ProfileBuilder} + */ +public final class ProfileBuilderImpl implements ProfileBuilder { + + private static final String PARENTS_ATTRIBUTE_KEY = Profile.ATTRIBUTE_PREFIX + Profile.PARENTS; + + private String profileId; + private Map<String, byte[]> fileMapping = new HashMap<>(); + private boolean isOverlay; + + @Override + public ProfileBuilder from(Profile profile) { + profileId = profile.getId(); + setFileConfigurations(profile.getFileConfigurations()); + return this; + } + + @Override + public ProfileBuilder identity(String profileId) { + this.profileId = profileId; + return this; + } + + @Override + public List<String> getParents() { + Map<String, String> config = getConfigurationInternal(Profile.INTERNAL_PID); + String pspec = config.get(PARENTS_ATTRIBUTE_KEY); + String[] parentIds = pspec != null ? pspec.split(" ") : new String[0]; + return Arrays.asList(parentIds); + } + + @Override + public ProfileBuilder addParent(String parentId) { + return addParentsInternal(Collections.singletonList(parentId), false); + } + + @Override + public ProfileBuilder addParents(List<String> parentIds) { + return addParentsInternal(parentIds, false); + } + + @Override + public ProfileBuilder setParents(List<String> parentIds) { + return addParentsInternal(parentIds, true); + } + + private ProfileBuilder addParentsInternal(List<String> parentIds, boolean clear) { + Set<String> currentIds = new LinkedHashSet<>(getParents()); + if (clear) { + currentIds.clear(); + } + if (parentIds != null) { + currentIds.addAll(parentIds); + } + updateParentsAttribute(currentIds); + return this; + } + + @Override + public ProfileBuilder removeParent(String profileId) { + Set<String> currentIds = new LinkedHashSet<>(getParents()); + currentIds.remove(profileId); + updateParentsAttribute(currentIds); + return this; + } + + private void updateParentsAttribute(Collection<String> parentIds) { + Map<String, String> config = getConfigurationInternal(Profile.INTERNAL_PID); + config.remove(PARENTS_ATTRIBUTE_KEY); + if (parentIds.size() > 0) { + config.put(PARENTS_ATTRIBUTE_KEY, parentsAttributeValue(parentIds)); + } + addConfiguration(Profile.INTERNAL_PID, config); + } + + private String parentsAttributeValue(Collection<String> parentIds) { + String pspec = ""; + if (parentIds.size() > 0) { + for (String parentId : parentIds) { + pspec += " " + parentId; + } + pspec = pspec.substring(1); + } + return pspec; + } + + @Override + public Set<String> getFileConfigurationKeys() { + return fileMapping.keySet(); + } + + @Override + public byte[] getFileConfiguration(String key) { + return fileMapping.get(key); + } + + @Override + public ProfileBuilder setFileConfigurations(Map<String, byte[]> configurations) { + fileMapping = new HashMap<>(configurations); + return this; + } + + @Override + public ProfileBuilder addFileConfiguration(String fileName, byte[] data) { + fileMapping.put(fileName, data); + return this; + } + + @Override + public ProfileBuilder deleteFileConfiguration(String fileName) { + fileMapping.remove(fileName); + return this; + } + + @Override + public ProfileBuilder setConfigurations(Map<String, Map<String, String>> configs) { + for (String pid : getConfigurationKeys()) { + deleteConfiguration(pid); + } + for (Entry<String, Map<String, String>> entry : configs.entrySet()) { + addConfiguration(entry.getKey(), new HashMap<>(entry.getValue())); + } + return this; + } + + @Override + public ProfileBuilder addConfiguration(String pid, Map<String, String> config) { + fileMapping.put(pid + Profile.PROPERTIES_SUFFIX, Utils.toBytes(config)); + return this; + } + + @Override + public ProfileBuilder addConfiguration(String pid, String key, String value) { + Map<String, String> config = getConfigurationInternal(pid); + config.put(key, value); + return addConfiguration(pid, config); + } + + @Override + public Set<String> getConfigurationKeys() { + Set<String> result = new HashSet<>(); + for (String fileKey : fileMapping.keySet()) { + if (fileKey.endsWith(Profile.PROPERTIES_SUFFIX)) { + String configKey = fileKey.substring(0, fileKey.indexOf(Profile.PROPERTIES_SUFFIX)); + result.add(configKey); + } + } + return Collections.unmodifiableSet(result); + } + + @Override + public Map<String, String> getConfiguration(String pid) { + Map<String, String> config = getConfigurationInternal(pid); + return Collections.unmodifiableMap(config); + } + + private Map<String, String> getConfigurationInternal(String pid) { + byte[] bytes = fileMapping.get(pid + Profile.PROPERTIES_SUFFIX); + return Utils.toProperties(bytes); + } + + @Override + public ProfileBuilder deleteConfiguration(String pid) { + fileMapping.remove(pid + Profile.PROPERTIES_SUFFIX); + return this; + } + + @Override + public ProfileBuilder setBundles(List<String> values) { + addAgentConfiguration(ConfigListType.BUNDLES, values); + return this; + } + + @Override + public ProfileBuilder addBundle(String value) { + addAgentConfiguration(ConfigListType.BUNDLES, value); + return this; + } + + @Override + public ProfileBuilder setFeatures(List<String> values) { + addAgentConfiguration(ConfigListType.FEATURES, values); + return this; + } + + @Override + public ProfileBuilder addFeature(String value) { + addAgentConfiguration(ConfigListType.FEATURES, value); + return this; + } + + @Override + public ProfileBuilder setRepositories(List<String> values) { + addAgentConfiguration(ConfigListType.REPOSITORIES, values); + return this; + } + + @Override + public ProfileBuilder addRepository(String value) { + addAgentConfiguration(ConfigListType.REPOSITORIES, value); + return this; + } + + @Override + public ProfileBuilder setOverrides(List<String> values) { + addAgentConfiguration(ConfigListType.OVERRIDES, values); + return this; + } + + @Override + public ProfileBuilder setOptionals(List<String> values) { + addAgentConfiguration(ConfigListType.OPTIONALS, values); + return this; + } + + public ProfileBuilder setOverlay(boolean overlay) { + this.isOverlay = overlay; + return this; + } + + @Override + public ProfileBuilder addAttribute(String key, String value) { + addConfiguration(Profile.INTERNAL_PID, Profile.ATTRIBUTE_PREFIX + key, value); + return this; + } + + @Override + public ProfileBuilder setAttributes(Map<String, String> attributes) { + Map<String, String> config = getConfigurationInternal(Profile.INTERNAL_PID); + for (String key : new ArrayList<>(config.keySet())) { + if (key.startsWith(Profile.ATTRIBUTE_PREFIX)) { + config.remove(key); + } + } + for (Entry<String, String> entry : attributes.entrySet()) { + config.put(Profile.ATTRIBUTE_PREFIX + entry.getKey(), entry.getValue()); + } + addConfiguration(Profile.INTERNAL_PID, config); + return null; + } + + private void addAgentConfiguration(ConfigListType type, List<String> values) { + String prefix = type + "."; + Map<String, String> config = getConfigurationInternal(Profile.INTERNAL_PID); + for (String key : new ArrayList<>(config.keySet())) { + if (key.startsWith(prefix)) { + config.remove(key); + } + } + for (String value : values) { + config.put(prefix + value, value); + } + addConfiguration(Profile.INTERNAL_PID, config); + } + + private void addAgentConfiguration(ConfigListType type, String value) { + String prefix = type + "."; + Map<String, String> config = getConfigurationInternal(Profile.INTERNAL_PID); + config.put(prefix + value, value); + addConfiguration(Profile.INTERNAL_PID, config); + } + + + @Override + public Profile getProfile() { + return new ProfileImpl(profileId, getParents(), fileMapping, isOverlay); + } + +}
http://git-wip-us.apache.org/repos/asf/karaf/blob/90cb480d/profile/src/main/java/org/apache/karaf/profile/impl/ProfileImpl.java ---------------------------------------------------------------------- diff --git a/profile/src/main/java/org/apache/karaf/profile/impl/ProfileImpl.java b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileImpl.java new file mode 100644 index 0000000..3533d8f --- /dev/null +++ b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileImpl.java @@ -0,0 +1,246 @@ +/* + * 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.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.zip.CRC32; + +import org.apache.karaf.profile.Profile; + +import static org.apache.karaf.profile.impl.Utils.assertNotNull; +import static org.apache.karaf.profile.impl.Utils.assertTrue; + + +/** + * This immutable profile implementation. + */ +final class ProfileImpl implements Profile { + + private static final Pattern ALLOWED_PROFILE_NAMES_PATTERN = Pattern.compile("^[A-Za-z0-9]+[\\.A-Za-z0-9_-]*$"); + + private final String profileId; + private final Map<String, String> attributes = new HashMap<>(); + private final List<String> parents = new ArrayList<>(); + private final Map<String, byte[]> fileConfigurations = new HashMap<>(); + private final Map<String, Map<String, String>> configurations = new HashMap<>(); + private final boolean isOverlay; + private int hash; + + // Only the {@link ProfileBuilder} should construct this + ProfileImpl(String profileId, List<String> parents, Map<String, byte[]> fileConfigs, boolean isOverlay) { + + assertNotNull(profileId, "profileId is null"); + assertNotNull(parents, "parents is null"); + assertNotNull(fileConfigs, "fileConfigs is null"); + assertTrue(ALLOWED_PROFILE_NAMES_PATTERN.matcher(profileId).matches(), "Profile id '" + profileId + "' is invalid. Profile id must be: lower-case letters, numbers, and . _ or - characters"); + + this.profileId = profileId; + this.isOverlay = isOverlay; + + // Parents + this.parents.addAll(parents); + + // File configurations and derived configurations + for (Entry<String, byte[]> entry : fileConfigs.entrySet()) { + String fileKey = entry.getKey(); + byte[] bytes = entry.getValue(); + fileConfigurations.put(fileKey, bytes); + if (fileKey.endsWith(Profile.PROPERTIES_SUFFIX)) { + String pid = fileKey.substring(0, fileKey.indexOf(Profile.PROPERTIES_SUFFIX)); + configurations.put(pid, Collections.unmodifiableMap(Utils.toProperties(bytes))); + } + } + + // Attributes are agent configuration with prefix 'attribute.' + Map<String, String> agentConfig = configurations.get(Profile.INTERNAL_PID); + if (agentConfig != null) { + int prefixLength = Profile.ATTRIBUTE_PREFIX.length(); + for (Entry<String, String> entry : agentConfig.entrySet()) { + String key = entry.getKey(); + if (key.startsWith(Profile.ATTRIBUTE_PREFIX)) { + attributes.put(key.substring(prefixLength), entry.getValue()); + } + } + } + } + + public String getId() { + return profileId; + } + + @Override + public Map<String, String> getAttributes() { + return Collections.unmodifiableMap(attributes); + } + + @Override + public List<String> getLibraries() { + return getContainerConfigList(ConfigListType.LIBRARIES); + } + + @Override + public List<String> getEndorsedLibraries() { + return getContainerConfigList(ConfigListType.ENDORSED); + } + + @Override + public List<String> getExtensionLibraries() { + return getContainerConfigList(ConfigListType.EXTENSION); + } + + @Override + public List<String> getBundles() { + return getContainerConfigList(ConfigListType.BUNDLES); + } + + @Override + public List<String> getFeatures() { + return getContainerConfigList(ConfigListType.FEATURES); + } + + @Override + public List<String> getRepositories() { + return getContainerConfigList(ConfigListType.REPOSITORIES); + } + + @Override + public List<String> getOverrides() { + return getContainerConfigList(ConfigListType.OVERRIDES); + } + + @Override + public List<String> getOptionals() { + return getContainerConfigList(ConfigListType.OPTIONALS); + } + + @Override + public List<String> getParentIds() { + return Collections.unmodifiableList(parents); + } + + @Override + public boolean isAbstract() { + return Boolean.parseBoolean(getAttributes().get(ABSTRACT)); + } + + @Override + public boolean isHidden() { + return Boolean.parseBoolean(getAttributes().get(HIDDEN)); + } + + public boolean isOverlay() { + return isOverlay; + } + + @Override + public Map<String, byte[]> getFileConfigurations() { + return Collections.unmodifiableMap(fileConfigurations); + } + + @Override + public Set<String> getConfigurationFileNames() { + return Collections.unmodifiableSet(fileConfigurations.keySet()); + } + + @Override + public byte[] getFileConfiguration(String fileName) { + return fileConfigurations.get(fileName); + } + + public Map<String, Map<String, String>> getConfigurations() { + return Collections.unmodifiableMap(configurations); + } + + @Override + public Map<String, String> getConfiguration(String pid) { + Map<String, String> config = configurations.get(pid); + config = config != null ? config : Collections.<String, String> emptyMap(); + return Collections.unmodifiableMap(config); + } + + private List<String> getContainerConfigList(ConfigListType type) { + Map<String, String> containerProps = getConfiguration(Profile.INTERNAL_PID); + List<String> rc = new ArrayList<>(); + String prefix = type + "."; + for (Map.Entry<String, String> e : containerProps.entrySet()) { + if ((e.getKey()).startsWith(prefix)) { + rc.add(e.getValue()); + } + } + return rc; + } + + @Override + public int hashCode() { + if (hash == 0) { + CRC32 crc = new CRC32(); + crc.update(profileId.getBytes()); + List<String> keys = new ArrayList<>(fileConfigurations.keySet()); + Collections.sort(keys); + for (String key : keys) { + crc.update(key.getBytes()); + crc.update(fileConfigurations.get(key)); + } + hash = (int) crc.getValue(); + } + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ProfileImpl)) return false; + ProfileImpl other = (ProfileImpl) obj; + + // Equality based on identity + return profileId.equals(other.profileId) + && fileConfigurations.equals(other.fileConfigurations); + } + + @Override + public String toString() { + return "Profile[id=" + profileId + ",attrs=" + getAttributes() + "]"; + } + + enum ConfigListType { + BUNDLES("bundle"), + ENDORSED("endorsed"), + EXTENSION("extension"), + FEATURES("feature"), + LIBRARIES("lib"), + OPTIONALS("optional"), + OVERRIDES("override"), + REPOSITORIES("repository"); + + private String value; + + private ConfigListType(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } +} http://git-wip-us.apache.org/repos/asf/karaf/blob/90cb480d/profile/src/main/java/org/apache/karaf/profile/impl/ProfileServiceImpl.java ---------------------------------------------------------------------- diff --git a/profile/src/main/java/org/apache/karaf/profile/impl/ProfileServiceImpl.java b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileServiceImpl.java new file mode 100644 index 0000000..c83e29a --- /dev/null +++ b/profile/src/main/java/org/apache/karaf/profile/impl/ProfileServiceImpl.java @@ -0,0 +1,246 @@ +/* + * 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.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.karaf.profile.LockHandle; +import org.apache.karaf.profile.PlaceholderResolver; +import org.apache.karaf.profile.Profile; +import org.apache.karaf.profile.ProfileService; + +import static org.apache.karaf.profile.impl.Utils.assertFalse; +import static org.apache.karaf.profile.impl.Utils.assertNotNull; +import static org.apache.karaf.profile.impl.Utils.join; + +public class ProfileServiceImpl implements ProfileService { + + private static final long ACQUIRE_LOCK_TIMEOUT = 25 * 1000L; + + private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + private final List<PlaceholderResolver> resolvers = new CopyOnWriteArrayList<>(); + private final Path profilesDirectory; + private Map<String, Profile> cache; + + public ProfileServiceImpl(Path profilesDirectory) throws IOException { + this.profilesDirectory = profilesDirectory; + Files.createDirectories(profilesDirectory); + } + + @Override + public LockHandle acquireWriteLock() { + return acquireLock(getLock().writeLock(), "Cannot obtain profile write lock in time"); + } + + @Override + public LockHandle acquireReadLock() { + return acquireLock(getLock().readLock(), "Cannot obtain profile read lock in time"); + } + + protected LockHandle acquireLock(final Lock lock, String message) { + try { + if (!lock.tryLock(ACQUIRE_LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) { + throw new IllegalStateException(message); + } + } catch (InterruptedException ex) { + throw new IllegalStateException(message, ex); + } + return new LockHandle() { + @Override + public void close() { + lock.unlock(); + } + }; + } + + protected ReadWriteLock getLock() { + return readWriteLock; + } + + @Override + public void registerResolver(PlaceholderResolver resolver) { + resolvers.add(resolver); + } + + @Override + public void unregisterResolver(PlaceholderResolver resolver) { + resolvers.remove(resolver); + } + + @Override + @SuppressWarnings("unused") + public void createProfile(Profile profile) { + assertNotNull(profile, "profile is null"); + try (LockHandle lock = acquireWriteLock()) { + String profileId = profile.getId(); + assertFalse(hasProfile(profileId), "Profile already exists: " + profileId); + createOrUpdateProfile(null, profile); + } + } + + @Override + @SuppressWarnings("unused") + public void updateProfile(Profile profile) { + assertNotNull(profile, "profile is null"); + try (LockHandle lock = acquireWriteLock()) { + final String profileId = profile.getId(); + final Profile lastProfile = getRequiredProfile(profileId); + createOrUpdateProfile(lastProfile, profile); + } + } + + @Override + @SuppressWarnings("unused") + public boolean hasProfile(String profileId) { + assertNotNull(profileId, "profileId is null"); + try (LockHandle lock = acquireReadLock()) { + Profile profile = getProfileFromCache(profileId); + return profile != null; + } + } + + @Override + @SuppressWarnings("unused") + public Profile getProfile(String profileId) { + assertNotNull(profileId, "profileId is null"); + try (LockHandle lock = acquireReadLock()) { + return getProfileFromCache(profileId); + } + } + + @Override + @SuppressWarnings("unused") + public Profile getRequiredProfile(String profileId) { + assertNotNull(profileId, "profileId is null"); + try (LockHandle lock = acquireReadLock()) { + Profile profile = getProfileFromCache(profileId); + assertNotNull(profile, "Profile does not exist: " + profileId); + return profile; + } + } + + @Override + @SuppressWarnings("unused") + public Collection<String> getProfiles() { + try (LockHandle lock = acquireReadLock()) { + Collection<String> profiles = getProfilesFromCache(); + return Collections.unmodifiableCollection(profiles); + } + } + + @Override + @SuppressWarnings("unused") + public void deleteProfile(String profileId) { + assertNotNull(profileId, "profileId is null"); + try (LockHandle lock = acquireWriteLock()) { + final Profile lastProfile = getRequiredProfile(profileId); + deleteProfileFromCache(lastProfile); + } + } + + @Override + public Profile getOverlayProfile(Profile profile) { + return Profiles.getOverlay(profile, loadCache()); + } + + @Override + public Profile getOverlayProfile(Profile profile, String environment) { + return Profiles.getOverlay(profile, loadCache(), environment); + } + + @Override + public Profile getEffectiveProfile(Profile profile) { + return Profiles.getEffective(profile, resolvers); + } + + @Override + public Profile getEffectiveProfile(Profile profile, boolean defaultsToEmptyString) { + return Profiles.getEffective(profile, resolvers, defaultsToEmptyString); + } + + protected void createOrUpdateProfile(Profile lastProfile, Profile profile) { + if (lastProfile != null) { + deleteProfileFromCache(lastProfile); + } + try { + loadCache(); + for (String parentId : profile.getParentIds()) { + if (!cache.containsKey(parentId)) { + throw new IllegalStateException("Parent profile " + parentId + " does not exist"); + } + } + Profiles.writeProfile(profilesDirectory, profile); + if (cache != null) { + cache.put(profile.getId(), profile); + } + } catch (IOException e) { + throw new IllegalStateException("Error writing profiles", e); + } + } + + protected Profile getProfileFromCache(String profileId) { + return loadCache().get(profileId); + } + + protected Collection<String> getProfilesFromCache() { + return loadCache().keySet(); + } + + protected void deleteProfileFromCache(Profile lastProfile) { + loadCache(); + List<String> children = new ArrayList<>(); + for (Profile p : cache.values()) { + if (p.getParentIds().contains(lastProfile.getId())) { + children.add(p.getId()); + } + } + if (!children.isEmpty()) { + throw new IllegalStateException("Profile " + lastProfile.getId() + " is a parent of " + join(", ", children)); + } + try { + Profiles.deleteProfile(profilesDirectory, lastProfile.getId()); + cache.remove(lastProfile.getId()); + } catch (IOException e) { + cache = null; + throw new IllegalStateException("Error deleting profiles", e); + } + } + + protected Map<String, Profile> loadCache() { + if (cache == null) { + try { + cache = Profiles.loadProfiles(profilesDirectory); + } catch (IOException e) { + throw new IllegalStateException("Error reading profiles", e); + } + } + return cache; + } + +} http://git-wip-us.apache.org/repos/asf/karaf/blob/90cb480d/profile/src/main/java/org/apache/karaf/profile/impl/Profiles.java ---------------------------------------------------------------------- diff --git a/profile/src/main/java/org/apache/karaf/profile/impl/Profiles.java b/profile/src/main/java/org/apache/karaf/profile/impl/Profiles.java new file mode 100644 index 0000000..1f6b507 --- /dev/null +++ b/profile/src/main/java/org/apache/karaf/profile/impl/Profiles.java @@ -0,0 +1,411 @@ +/* + * 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.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.AbstractMap; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.felix.utils.properties.InterpolationHelper; +import org.apache.felix.utils.properties.Properties; +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; + +public final class Profiles { + + public static final String PROFILE_FOLDER_SUFFIX = ".profile"; + + public static Map<String, Profile> loadProfiles(final Path root) throws IOException { + final Map<String, Profile> profiles = new HashMap<>(); + Files.walkFileTree(root, new SimpleFileVisitor<Path>() { + ProfileBuilder builder; + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (dir.getFileName().toString().endsWith(PROFILE_FOLDER_SUFFIX)) { + String profileId = root.relativize(dir).toString(); + profileId = profileId.replaceAll(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; + } + + public static void deleteProfile(Path root, String id) throws IOException { + Path path = root.resolve(id.replaceAll("-", root.getFileSystem().getSeparator()) + PROFILE_FOLDER_SUFFIX); + if (Files.isDirectory(path)) { + 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; + } + }); + } + } + + public static void writeProfile(Path root, Profile profile) throws IOException { + Path path = root.resolve(profile.getId().replaceAll("-", root.getFileSystem().getSeparator()) + PROFILE_FOLDER_SUFFIX); + Files.createDirectories(path); + for (Map.Entry<String, byte[]> entry : profile.getFileConfigurations().entrySet()) { + Files.write(path.resolve(entry.getKey()), entry.getValue(), StandardOpenOption.CREATE_NEW); + } + } + + public static Profile getOverlay(Profile profile, Map<String, Profile> profiles) { + return getOverlay(profile, profiles, null); + } + + 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(); + } + } + + public static Profile getEffective(final Profile profile) { + return getEffective(profile, + true); + } + + public static Profile getEffective(final Profile profile, boolean finalSubstitution) { + return getEffective(profile, + Collections.<PlaceholderResolver>singleton(new PlaceholderResolvers.ProfilePlaceholderResolver()), + finalSubstitution); + } + + public static Profile getEffective(final Profile profile, + final Collection<PlaceholderResolver> resolvers) { + return getEffective(profile, resolvers, true); + } + + public static Profile getEffective(final Profile profile, + final Collection<PlaceholderResolver> resolvers, + boolean finalSubstitution) { + assertNotNull(profile, "profile is null"); + assertNotNull(profile, "resolvers is null"); + // Build dynamic configurations which can support lazy computation of substituted values + final Map<String, Map<String, String>> dynamic = new HashMap<>(); + for (Map.Entry<String, Map<String, String>> cfg : profile.getConfigurations().entrySet()) { + dynamic.put(cfg.getKey(), new DynamicMap(dynamic, cfg.getKey(), cfg.getValue(), resolvers, finalSubstitution)); + } + // Compute the new profile + return ProfileBuilder.Factory.createFrom(profile) + .setConfigurations(dynamic) + .getProfile(); + } + + private static class DynamicMap extends AbstractMap<String, String> { + + private final Map<String, String> computed = new HashMap<>(); + private final Map<String, String> cycles = new HashMap<>(); + private final Map<String, Map<String, String>> profile; + private final String pid; + private final Map<String, String> original; + private final Collection<PlaceholderResolver> resolvers; + private final boolean finalSubstitution; + + private DynamicMap(Map<String, Map<String, String>> profile, + String pid, + Map<String, String> original, + Collection<PlaceholderResolver> resolvers, + boolean finalSubstitution) { + this.profile = profile; + this.pid = pid; + this.original = original; + this.resolvers = resolvers; + this.finalSubstitution = finalSubstitution; + } + + @Override + public Set<Entry<String, String>> entrySet() { + return new DynamicEntrySet(); + } + + private class DynamicEntrySet extends AbstractSet<Entry<String, String>> { + + @Override + public Iterator<Entry<String, String>> iterator() { + return new DynamicEntrySetIterator(); + } + + @Override + public int size() { + return original.size(); + } + + } + + private class DynamicEntrySetIterator implements Iterator<Entry<String, String>> { + final Iterator<Entry<String, String>> delegate = original.entrySet().iterator(); + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public Entry<String, String> next() { + final Entry<String, String> original = delegate.next(); + return new DynamicEntry(original.getKey(), original.getValue()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private class DynamicEntry implements Entry<String, String> { + + private final String key; + private final String value; + + private DynamicEntry(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public String getKey() { + return key; + } + + @Override + public String getValue() { + String v = computed.get(key); + if (v == null) { + v = compute(); + computed.put(key, v); + } + return v; + } + + private String compute() { + InterpolationHelper.SubstitutionCallback callback = new InterpolationHelper.SubstitutionCallback() { + public String getValue(String value) { + if (value != null) { + for (PlaceholderResolver resolver : resolvers) { + if (resolver.getScheme() == null) { + String val = resolver.resolve(profile, 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(profile, pid, key, toSubst); + if (val != null) { + return val; + } + } + } + } + } + return null; + } + }; + String v = InterpolationHelper.substVars(value, key, cycles, DynamicMap.this, callback, finalSubstitution, finalSubstitution, finalSubstitution); + for (PlaceholderResolver resolver : resolvers) { + if (PlaceholderResolver.CATCH_ALL_SCHEME.equals(resolver.getScheme())) { + String val = resolver.resolve(profile, pid, key, v); + if (val != null) { + v = val; + } + } + } + return v; + } + + @Override + public String setValue(String value) { + throw new UnsupportedOperationException(); + } + } + } + + static private class OverlayOptionsProvider { + + private final Map<String, Profile> profiles; + private final Profile self; + private final String environment; + + private static class SupplementControl { + byte[] data; + Properties 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.. + Properties childMap = Utils.toProperties(value); + if (childMap.remove(Profile.DELETED) != null) { + ctrl.props.clear(); + } + + // Update the entries... + for (Map.Entry<String, String> 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() { } + +} http://git-wip-us.apache.org/repos/asf/karaf/blob/90cb480d/profile/src/main/java/org/apache/karaf/profile/impl/Utils.java ---------------------------------------------------------------------- diff --git a/profile/src/main/java/org/apache/karaf/profile/impl/Utils.java b/profile/src/main/java/org/apache/karaf/profile/impl/Utils.java new file mode 100644 index 0000000..0765718 --- /dev/null +++ b/profile/src/main/java/org/apache/karaf/profile/impl/Utils.java @@ -0,0 +1,103 @@ +/* + * 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.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Map; + +import org.apache.felix.utils.properties.Properties; + +public final class Utils { + + private Utils() { } + + public static void assertNotNull(Object object, String message) { + if (object == null) { + throw new IllegalStateException(message); + } + } + + public static void assertTrue(boolean condition, String message) { + if (!condition) { + throw new IllegalStateException(message); + } + } + + public static void assertFalse(boolean condition, String message) { + if (condition) { + throw new IllegalStateException(message); + } + } + + public static String join(CharSequence sep, Iterable<? extends CharSequence> strings) { + StringBuilder sb = new StringBuilder(); + for (CharSequence str : strings) { + if (sb.length() > 0) { + sb.append(sep); + } + sb.append(str); + } + return sb.toString(); + } + + + public static byte[] toBytes(Properties source) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + source.store(baos, null); + } catch (IOException ex) { + throw new IllegalArgumentException("Cannot store properties", ex); + } + return baos.toByteArray(); + } + + public static byte[] toBytes(Map<String, String> source) { + return toBytes(toProperties(source)); + } + + public static Properties toProperties(byte[] source) { + try { + Properties rc = new Properties(false); + if (source != null) { + rc.load(new ByteArrayInputStream(source)); + } + return rc; + } catch (IOException ex) { + throw new IllegalArgumentException("Cannot load properties", ex); + } + } + + public static Properties toProperties(Map<String, String> source) { + try { + Properties rc = new Properties(false); + rc.putAll(source); + return rc; + } catch (IOException ex) { + throw new IllegalArgumentException("Cannot load properties", ex); + } + } + + public static String stripSuffix(String value, String suffix) { + if (value.endsWith(suffix)) { + return value.substring(0, value.length() - suffix.length()); + } else { + return value; + } + } +} http://git-wip-us.apache.org/repos/asf/karaf/blob/90cb480d/profile/src/main/java/org/apache/karaf/profile/impl/osgi/Activator.java ---------------------------------------------------------------------- diff --git a/profile/src/main/java/org/apache/karaf/profile/impl/osgi/Activator.java b/profile/src/main/java/org/apache/karaf/profile/impl/osgi/Activator.java new file mode 100644 index 0000000..9d0bae2 --- /dev/null +++ b/profile/src/main/java/org/apache/karaf/profile/impl/osgi/Activator.java @@ -0,0 +1,44 @@ +/* + * 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.osgi; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.karaf.profile.ProfileService; +import org.apache.karaf.profile.impl.ProfileServiceImpl; +import org.apache.karaf.util.tracker.BaseActivator; +import org.apache.karaf.util.tracker.Managed; +import org.apache.karaf.util.tracker.ProvideService; +import org.apache.karaf.util.tracker.Services; +import org.osgi.service.cm.ManagedService; + +@Services( + provides = @ProvideService(ProfileService.class) +) +@Managed("org.apache.karaf.profile") +public class Activator extends BaseActivator implements ManagedService { + + @Override + protected void doStart() throws Exception { + Path root = Paths.get(getString("profilesDirectory", System.getProperty("karaf.home") + "/profiles")); + ProfileServiceImpl service = new ProfileServiceImpl(root); + register(ProfileService.class, service); + } + +} http://git-wip-us.apache.org/repos/asf/karaf/blob/90cb480d/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java ---------------------------------------------------------------------- diff --git a/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java b/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java new file mode 100644 index 0000000..0b4ccd3 --- /dev/null +++ b/profile/src/test/java/org/apache/karaf/profile/impl/ProfilesTest.java @@ -0,0 +1,90 @@ +/* + * 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.util.HashMap; +import java.util.Map; + +import org.apache.karaf.profile.Profile; +import org.apache.karaf.profile.ProfileBuilder; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ProfilesTest { + + @Test + public void testProfilePlaceholderResolver() { + Profile profile = ProfileBuilder.Factory.create("test") + .addConfiguration("pid1", "foo", "b${profile:pid2/bar}") + .addConfiguration("pid2", "bar", "a${rep}") + .addConfiguration("pid2", "rep", "h") + .getProfile(); + + Profile effective = Profiles.getEffective(profile); + + assertEquals("bah", effective.getConfiguration("pid1").get("foo")); + } + + @Test(expected = IllegalArgumentException.class) + public void testProfilePlaceholderResolverWitCycle() { + Profile profile = ProfileBuilder.Factory.create("test") + .addConfiguration("pid1", "foo", "b${profile:pid2/bar}") + .addConfiguration("pid2", "bar", "a${rep}") + .addConfiguration("pid2", "rep", "h${profile:pid1/foo}") + .getProfile(); + + Profile effective = Profiles.getEffective(profile); + + effective.getConfiguration("pid1").get("foo"); + // Should throw an exception + } + + @Test + public void testNonSubstitution() { + Profile profile = ProfileBuilder.Factory.create("test") + .addConfiguration("pid1", "key", "${foo}/${bar}") + .getProfile(); + + Profile effective = Profiles.getEffective(profile, false); + + assertEquals("${foo}/${bar}", effective.getConfiguration("pid1").get("key")); + } + + @Test + public void testProfilesOverlayComments() { + String pid1 = "# My comment\nfoo = bar\n"; + + Profile parent = ProfileBuilder.Factory.create("parent") + .addFileConfiguration("pid1.cfg", pid1.getBytes()) + .getProfile(); + + Profile profile = ProfileBuilder.Factory.create("test") + .addConfiguration("pid1", "foo", "bar2") + .addParent("parent") + .getProfile(); + + Map<String, Profile> profiles = new HashMap<>(); + profiles.put(parent.getId(), parent); + profiles.put(profile.getId(), profile); + + Profile overlay = Profiles.getOverlay(profile, profiles); + + String outPid1 = new String(overlay.getFileConfiguration("pid1.cfg")); + assertEquals("# My comment\nfoo = bar2\n", outPid1); + } +} http://git-wip-us.apache.org/repos/asf/karaf/blob/90cb480d/tooling/karaf-maven-plugin/pom.xml ---------------------------------------------------------------------- diff --git a/tooling/karaf-maven-plugin/pom.xml b/tooling/karaf-maven-plugin/pom.xml index 5e5949a..188f2ae 100644 --- a/tooling/karaf-maven-plugin/pom.xml +++ b/tooling/karaf-maven-plugin/pom.xml @@ -134,6 +134,10 @@ </exclusions> </dependency> <dependency> + <groupId>org.apache.karaf.profile</groupId> + <artifactId>org.apache.karaf.profile.core</artifactId> + </dependency> + <dependency> <groupId>org.apache.felix</groupId> <artifactId>org.apache.felix.resolver</artifactId> </dependency>