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>

Reply via email to