brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From s...@apache.org
Subject [1/9] brooklyn-server git commit: Makes config parent-inheritance “merge” configurable
Date Mon, 06 Jun 2016 14:45:55 GMT
Repository: brooklyn-server
Updated Branches:
  refs/heads/master 2276de020 -> 2edf7168d


Makes config parent-inheritance “merge” configurable

Adds:
* ConfigKey.getParentInheritance (deprecating getInheritance)
* ConfigKey.getTypeInheritance (not yet implemented)
* Adds MapConfigKey.Builder
* Some SoftwareProcess config keys now set typeInheritance(MERGE)

Breaking backwards-compatibility changes:
* ConfigInheritance.isInherited return type changed (method was @Beta)
* VanillaSoftwareProcess keys changed to MapConfigKey, rather than
  ConfigKey<Map>
* BasicConfigKey.Builder: generics changed, to allow sub-typing by
  MapConfigKey.Builder
* AbstractStructuredConfigKey.subType field changed from public to
  protected

Change of semantics:
* Previously, when looking up entity config, the order of preference
  (in EntityConfigMap) was:
   1. look up own config, using key
   2. look up inherited config, using key
   3. look up “own bag”, using key’s name
   4. look up “inherited bag”, using key’s name.
  Now the order is (1), (3), (2), (4).


Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/57beafa2
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/57beafa2
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/57beafa2

Branch: refs/heads/master
Commit: 57beafa24bcfa5ffe9db16fd8e9dcb51acba0eab
Parents: 97807f2
Author: Aled Sage <aled.sage@gmail.com>
Authored: Fri May 27 09:43:53 2016 +0100
Committer: Aled Sage <aled.sage@gmail.com>
Committed: Mon Jun 6 15:10:08 2016 +0100

----------------------------------------------------------------------
 .../camp/brooklyn/BrooklynCampConstants.java    |   4 +-
 .../brooklyn/ConfigInheritanceYamlTest.java     | 244 ++++++++++++++++++-
 .../brooklyn/core/config/BasicConfigKey.java    | 169 +++++++++----
 .../apache/brooklyn/core/config/ConfigKeys.java |   8 +-
 .../brooklyn/core/config/MapConfigKey.java      |  77 +++++-
 .../internal/AbstractStructuredConfigKey.java   |   2 +-
 .../core/entity/BrooklynConfigKeys.java         |  11 +-
 .../core/entity/internal/EntityConfigMap.java   | 129 +++++++---
 .../entity/lifecycle/ServiceStateLogic.java     |   4 +-
 .../core/location/AbstractLocation.java         |   4 +-
 .../brooklyn/core/objs/BasicSpecParameter.java  |  15 +-
 .../sensor/AttributeSensorAndConfigKey.java     |   2 +-
 .../sensor/PortAttributeSensorAndConfigKey.java |   2 +-
 .../brooklyn/core/config/ConfigKeysTest.java    |   4 +-
 .../entity/ConfigEntityInheritanceTest.java     |   4 +-
 .../entity/software/base/SoftwareProcess.java   |  54 ++--
 .../brooklyn/config/ConfigInheritance.java      |  41 +++-
 .../org/apache/brooklyn/config/ConfigKey.java   |  12 +
 18 files changed, 643 insertions(+), 143 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/BrooklynCampConstants.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/BrooklynCampConstants.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/BrooklynCampConstants.java
index d3641f2..5a2d4e6 100644
--- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/BrooklynCampConstants.java
+++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/BrooklynCampConstants.java
@@ -34,12 +34,12 @@ public class BrooklynCampConstants {
     public static final ConfigKey<String> PLAN_ID = ConfigKeys.builder(String.class, "camp.plan.id")
             .description("Identifier supplied in the deployment plan for component to which this entity corresponds "
                         + "(human-readable, for correlating across plan, template, and instance)")
-            .inheritance(ConfigInheritance.NONE)
+            .parentInheritance(ConfigInheritance.NONE)
             .build();
 
     public static final ConfigKey<String> TEMPLATE_ID = ConfigKeys.builder(String.class, "camp.template.id")
             .description("UID of the component in the CAMP template from which this entity was created")
-            .inheritance(ConfigInheritance.NONE)
+            .parentInheritance(ConfigInheritance.NONE)
             .build();
 
     public static final ConfigKey<CampPlatform> CAMP_PLATFORM = ConfigKeys.newConfigKey(CampPlatform.class, "brooklyn.camp.platform",

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/ConfigInheritanceYamlTest.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/ConfigInheritanceYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/ConfigInheritanceYamlTest.java
index a9bbcb0..1d37589 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/ConfigInheritanceYamlTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/ConfigInheritanceYamlTest.java
@@ -31,6 +31,7 @@ import java.util.concurrent.Executors;
 
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.MapConfigKey;
 import org.apache.brooklyn.core.entity.EntityAsserts;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.core.test.entity.TestEntity;
@@ -112,8 +113,8 @@ public class ConfigInheritanceYamlTest extends AbstractYamlTest {
     public void tearDown() throws Exception {
         super.tearDown();
         if (executor != null) executor.shutdownNow();
-        if (emptyFile != null) Files.delete(emptyFile);
-        if (emptyFile2 != null) Files.delete(emptyFile2);
+        if (emptyFile != null) Files.deleteIfExists(emptyFile);
+        if (emptyFile2 != null) Files.deleteIfExists(emptyFile2);
     }
     
     @Test
@@ -134,6 +135,43 @@ public class ConfigInheritanceYamlTest extends AbstractYamlTest {
     }
     
     @Test
+    public void testExtendsSuperTypeConfig() throws Exception {
+        String yaml = Joiner.on("\n").join(
+                "location: localhost-stub",
+                "services:",
+                "- type: EmptySoftwareProcess-with-conf",
+                "  brooklyn.config:",
+                "    shell.env:",
+                "      ENV2: myEnv2",
+                "    templates.preinstall:",
+                "      "+emptyFile2.toUri()+": myfile2",
+                "    files.preinstall:",
+                "      "+emptyFile2.toUri()+": myfile2",
+                "    templates.install:",
+                "      "+emptyFile2.toUri()+": myfile2",
+                "    files.install:",
+                "      "+emptyFile2.toUri()+": myfile2",
+                "    templates.runtime:",
+                "      "+emptyFile2.toUri()+": myfile2",
+                "    files.runtime:",
+                "      "+emptyFile2.toUri()+": myfile2",
+                "    provisioning.properties:",
+                "      mykey2: myval2",
+                "      templateOptions:",
+                "        myOptionsKey2: myOptionsVal2");
+        
+        Entity app = createStartWaitAndLogApplication(new StringReader(yaml));
+        Entity entity = Iterables.getOnlyElement(app.getChildren());
+        
+        assertEmptySoftwareProcessConfig(
+                entity,
+                ImmutableMap.of("ENV1", "myEnv1", "ENV2", "myEnv2"),
+                ImmutableMap.of(emptyFile.toUri().toString(), "myfile", emptyFile2.toUri().toString(), "myfile2"),
+                ImmutableMap.of("mykey", "myval", "mykey2", "myval2", "templateOptions", 
+                        ImmutableMap.of("myOptionsKey", "myOptionsVal", "myOptionsKey2", "myOptionsVal2")));
+    }
+    
+    @Test
     public void testInheritsParentConfig() throws Exception {
         String yaml = Joiner.on("\n").join(
                 "location: localhost-stub",
@@ -300,14 +338,212 @@ public class ConfigInheritanceYamlTest extends AbstractYamlTest {
                 ImmutableMap.of("mykey2", "myval2", "templateOptions", ImmutableMap.of("myOptionsKey2", "myOptionsVal2")));
     }
     
+    @Test
+    public void testEntityTypeInheritanceOptions() throws Exception {
+        addCatalogItems(
+                "brooklyn.catalog:",
+                "  itemType: entity",
+                "  items:",
+                "  - id: entity-with-keys",
+                "    item:",
+                "      type: org.apache.brooklyn.core.test.entity.TestEntity",
+                "      brooklyn.parameters:",
+                "      - name: map.type-merged",
+                "        type: java.util.Map",
+                "        inheritance.type: merge",
+                "        default: {myDefaultKey: myDefaultVal}",
+                "      - name: map.type-always",
+                "        type: java.util.Map",
+                "        inheritance.type: always",
+                "        default: {myDefaultKey: myDefaultVal}",
+                "      - name: map.type-never",
+                "        type: java.util.Map",
+                "        inheritance.type: none",
+                "        default: {myDefaultKey: myDefaultVal}",
+                "  - id: entity-with-conf",
+                "    item:",
+                "      type: entity-with-keys",
+                "      brooklyn.config:",
+                "        map.type-merged:",
+                "          mykey: myval",
+                "        map.type-always:",
+                "          mykey: myval",
+                "        map.type-never:",
+                "          mykey: myval");
+        
+        // Test retrieval of defaults
+        {
+            String yaml = Joiner.on("\n").join(
+                    "services:",
+                    "- type: entity-with-keys");
+            
+            Entity app = createStartWaitAndLogApplication(new StringReader(yaml));
+            TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren());
+    
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-merged")), ImmutableMap.of("myDefaultKey", "myDefaultVal"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-always")), ImmutableMap.of("myDefaultKey", "myDefaultVal"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-never")), ImmutableMap.of("myDefaultKey", "myDefaultVal"));
+        }
+
+        // Test override defaults
+        {
+            String yaml = Joiner.on("\n").join(
+                    "services:",
+                    "- type: entity-with-keys",
+                    "  brooklyn.config:",
+                    "    map.type-merged:",
+                    "      mykey2: myval2",
+                    "    map.type-always:",
+                    "      mykey2: myval2",
+                    "    map.type-never:",
+                    "      mykey2: myval2");
+            
+            Entity app = createStartWaitAndLogApplication(new StringReader(yaml));
+            TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren());
+    
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-merged")), ImmutableMap.of("mykey2", "myval2"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-always")), ImmutableMap.of("mykey2", "myval2"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-never")), ImmutableMap.of("mykey2", "myval2"));
+        }
+
+        // Test retrieval of explicit config
+        // TODO what should "never" mean here? Should we get the default value?
+        {
+            String yaml = Joiner.on("\n").join(
+                    "services:",
+                    "- type: entity-with-conf");
+            
+            Entity app = createStartWaitAndLogApplication(new StringReader(yaml));
+            TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren());
+    
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-merged")), ImmutableMap.of("mykey", "myval"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-always")), ImmutableMap.of("mykey", "myval"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-never")), ImmutableMap.of("myDefaultKey", "myDefaultVal"));
+        }
+        
+        // Test merging/overriding of explicit config
+        {
+            String yaml = Joiner.on("\n").join(
+                    "services:",
+                    "- type: entity-with-conf",
+                    "  brooklyn.config:",
+                    "    map.type-merged:",
+                    "      mykey2: myval2",
+                    "    map.type-always:",
+                    "      mykey2: myval2",
+                    "    map.type-never:",
+                    "      mykey2: myval2");
+            
+            Entity app = createStartWaitAndLogApplication(new StringReader(yaml));
+            TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren());
+    
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-merged")), ImmutableMap.of("mykey", "myval", "mykey2", "myval2"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-always")), ImmutableMap.of("mykey2", "myval2"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-never")), ImmutableMap.of("mykey2", "myval2"));
+        }
+    }
+    
+    @Test
+    public void testParentInheritanceOptions() throws Exception {
+        addCatalogItems(
+                "brooklyn.catalog:",
+                "  itemType: entity",
+                "  items:",
+                "  - id: entity-with-keys",
+                "    item:",
+                "      type: org.apache.brooklyn.core.test.entity.TestEntity",
+                "      brooklyn.parameters:",
+                "      - name: map.type-merged",
+                "        type: java.util.Map",
+                "        inheritance.parent: merge",
+                "        default: {myDefaultKey: myDefaultVal}",
+                "      - name: map.type-always",
+                "        type: java.util.Map",
+                "        inheritance.parent: always",
+                "        default: {myDefaultKey: myDefaultVal}",
+                "      - name: map.type-never",
+                "        type: java.util.Map",
+                "        inheritance.parent: none",
+                "        default: {myDefaultKey: myDefaultVal}");
+        
+        // Test retrieval of defaults
+        {
+            String yaml = Joiner.on("\n").join(
+                    "services:",
+                    "- type: org.apache.brooklyn.entity.stock.BasicApplication",
+                    "  brooklyn.children:",
+                    "  - type: entity-with-keys");
+            
+            Entity app = createStartWaitAndLogApplication(new StringReader(yaml));
+            TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren());
+    
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-merged")), ImmutableMap.of("myDefaultKey", "myDefaultVal"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-always")), ImmutableMap.of("myDefaultKey", "myDefaultVal"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-never")), ImmutableMap.of("myDefaultKey", "myDefaultVal"));
+        }
+
+        // Test override defaults in parent entity
+        {
+            String yaml = Joiner.on("\n").join(
+                    "services:",
+                    "- type: org.apache.brooklyn.entity.stock.BasicApplication",
+                    "  brooklyn.config:",
+                    "    map.type-merged:",
+                    "      mykey: myval",
+                    "    map.type-always:",
+                    "      mykey: myval",
+                    "    map.type-never:",
+                    "      mykey: myval",
+                    "  brooklyn.children:",
+                    "  - type: entity-with-keys");
+            
+            Entity app = createStartWaitAndLogApplication(new StringReader(yaml));
+            TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren());
+    
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-merged")), ImmutableMap.of("mykey", "myval"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-always")), ImmutableMap.of("mykey", "myval"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-never")), ImmutableMap.of("myDefaultKey", "myDefaultVal"));
+        }
+
+        // Test merging/overriding of explicit config
+        {
+            String yaml = Joiner.on("\n").join(
+                    "services:",
+                    "- type: org.apache.brooklyn.entity.stock.BasicApplication",
+                    "  brooklyn.config:",
+                    "    map.type-merged:",
+                    "      mykey: myval",
+                    "    map.type-always:",
+                    "      mykey: myval",
+                    "    map.type-never:",
+                    "      mykey: myval",
+                    "  brooklyn.children:",
+                    "  - type: entity-with-keys",
+                    "    brooklyn.config:",
+                    "      map.type-merged:",
+                    "        mykey2: myval2",
+                    "      map.type-always:",
+                    "        mykey2: myval2",
+                    "      map.type-never:",
+                    "        mykey2: myval2");
+            
+            Entity app = createStartWaitAndLogApplication(new StringReader(yaml));
+            TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren());
+    
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-merged")), ImmutableMap.of("mykey", "myval", "mykey2", "myval2"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-always")), ImmutableMap.of("mykey2", "myval2"));
+            assertEquals(entity.config().get(entity.getEntityType().getConfigKey("map.type-never")), ImmutableMap.of("mykey2", "myval2"));
+        }
+    }
+    
     protected void assertEmptySoftwareProcessConfig(Entity entity, Map<String, ?> expectedEnv, Map<String, String> expectedFiles, Map<String, ?> expectedProvisioningProps) {
         EntityAsserts.assertConfigEquals(entity, EmptySoftwareProcess.SHELL_ENVIRONMENT, MutableMap.<String, Object>copyOf(expectedEnv));
         
-        List<ConfigKey<Map<String,String>>> keys = ImmutableList.of(EmptySoftwareProcess.PRE_INSTALL_TEMPLATES, 
+        List<MapConfigKey<String>> keys = ImmutableList.of(EmptySoftwareProcess.PRE_INSTALL_TEMPLATES, 
                 EmptySoftwareProcess.PRE_INSTALL_FILES, EmptySoftwareProcess.INSTALL_TEMPLATES, 
                 EmptySoftwareProcess.INSTALL_FILES, EmptySoftwareProcess.RUNTIME_TEMPLATES, 
                 EmptySoftwareProcess.RUNTIME_FILES);
-        for (ConfigKey<Map<String,String>> key : keys) {
+        for (MapConfigKey<String> key : keys) {
             EntityAsserts.assertConfigEquals(entity, key, expectedFiles);
         }
         

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/main/java/org/apache/brooklyn/core/config/BasicConfigKey.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/config/BasicConfigKey.java b/core/src/main/java/org/apache/brooklyn/core/config/BasicConfigKey.java
index f158c2c..a1285d8 100644
--- a/core/src/main/java/org/apache/brooklyn/core/config/BasicConfigKey.java
+++ b/core/src/main/java/org/apache/brooklyn/core/config/BasicConfigKey.java
@@ -54,66 +54,105 @@ public class BasicConfigKey<T> implements ConfigKeySelfExtracting<T>, Serializab
     
     private static final Splitter dots = Splitter.on('.');
 
-    public static <T> Builder<T> builder(TypeToken<T> type) {
-        return new Builder<T>().type(type);
+    public static <T> Builder<T,?> builder(TypeToken<T> type) {
+        return new ConcreteBuilder<T>().type(type);
     }
 
-    public static <T> Builder<T> builder(Class<T> type) {
-        return new Builder<T>().type(type);
+    public static <T> Builder<T,?> builder(Class<T> type) {
+        return new ConcreteBuilder<T>().type(type);
     }
 
-    public static <T> Builder<T> builder(TypeToken<T> type, String name) {
-        return new Builder<T>().type(type).name(name);
+    public static <T> Builder<T,?> builder(TypeToken<T> type, String name) {
+        return new ConcreteBuilder<T>(type, name);
     }
 
-    public static <T> Builder<T> builder(Class<T> type, String name) {
-        return new Builder<T>().type(type).name(name);
+    public static <T> Builder<T,?> builder(Class<T> type, String name) {
+        return new ConcreteBuilder<T>().type(type).name(name);
     }
 
-    public static <T> Builder<T> builder(ConfigKey<T> key) {
-        return new Builder<T>()
-            .name(checkNotNull(key.getName(), "name"))
-            .type(checkNotNull(key.getTypeToken(), "type"))
-            .description(key.getDescription())
-            .defaultValue(key.getDefaultValue())
-            .reconfigurable(key.isReconfigurable())
-            .inheritance(key.getInheritance())
-            .constraint(key.getConstraint());
+    public static <T> Builder<T,?> builder(ConfigKey<T> key) {
+        return new ConcreteBuilder<T>(key);
     }
 
-    public static class Builder<T> {
-        private String name;
-        private TypeToken<T> type;
-        private String description;
-        private T defaultValue;
-        private boolean reconfigurable;
-        private Predicate<? super T> constraint = Predicates.alwaysTrue();
-        private ConfigInheritance inheritance;
+    private static class ConcreteBuilder<T> extends Builder<T, ConcreteBuilder<T>> {
+        public ConcreteBuilder() {
+        }
+        public ConcreteBuilder(TypeToken<T> type, String name) {
+            super(type, name);
+        }
+        public ConcreteBuilder(ConfigKey<T> key) {
+            super(key);
+        }
+        @Override protected ConcreteBuilder<T> self() {
+            return this;
+        }
+    }
+
+    public abstract static class Builder<T, B extends Builder<T,B>> {
+        protected String name;
+        protected TypeToken<T> type;
+        protected String description;
+        protected T defaultValue;
+        protected boolean reconfigurable;
+        protected Predicate<? super T> constraint = Predicates.alwaysTrue();
+        protected ConfigInheritance parentInheritance;
+        protected ConfigInheritance typeInheritance;
         
-        public Builder<T> name(String val) {
-            this.name = val; return this;
+        protected abstract B self();
+
+        public Builder() {
+        }
+        public Builder(TypeToken<T> type, String name) {
+            this.type = type;
+            this.name = name;
+        }
+        public Builder(Class<T> type, String name) {
+            this(TypeToken.of(type), name);
+        }
+        public Builder(ConfigKey<T> key) {
+            this.type = checkNotNull(key.getTypeToken(), "type");
+            this.name = checkNotNull(key.getName(), "name");
+            description(key.getDescription());
+            defaultValue(key.getDefaultValue());
+            reconfigurable(key.isReconfigurable());
+            parentInheritance(key.getParentInheritance());
+            typeInheritance(key.getTypeInheritance());
+            constraint(key.getConstraint());
+        }
+        public B name(String val) {
+            this.name = val; return self();
+        }
+        public B type(Class<T> val) {
+            this.type = TypeToken.of(val); return self();
         }
-        public Builder<T> type(Class<T> val) {
-            this.type = TypeToken.of(val); return this;
+        public B type(TypeToken<T> val) {
+            this.type = val; return self();
         }
-        public Builder<T> type(TypeToken<T> val) {
-            this.type = val; return this;
+        public B description(String val) {
+            this.description = val; return self();
         }
-        public Builder<T> description(String val) {
-            this.description = val; return this;
+        public B defaultValue(T val) {
+            this.defaultValue = val; return self();
         }
-        public Builder<T> defaultValue(T val) {
-            this.defaultValue = val; return this;
+        public B reconfigurable(boolean val) {
+            this.reconfigurable = val; return self();
         }
-        public Builder<T> reconfigurable(boolean val) {
-            this.reconfigurable = val; return this;
+        public B parentInheritance(ConfigInheritance val) {
+            this.parentInheritance = val; return self();
         }
-        public Builder<T> inheritance(ConfigInheritance val) {
-            this.inheritance = val; return this;
+        public B typeInheritance(ConfigInheritance val) {
+            this.typeInheritance = val; return self();
+        }
+        /**
+         * @deprecated since 0.10.0; use {@link #parentInheritance(ConfigInheritance)}
+         */
+        @Deprecated
+        public B inheritance(ConfigInheritance val) {
+            return parentInheritance(val);
         }
         @Beta
-        public Builder<T> constraint(Predicate<? super T> constraint) {
-            this.constraint = checkNotNull(constraint, "constraint"); return this;
+        public B constraint(Predicate<? super T> constraint) {
+            this.constraint = checkNotNull(constraint, "constraint"); return self();
         }
         public BasicConfigKey<T> build() {
             return new BasicConfigKey<T>(this);
@@ -127,14 +166,22 @@ public class BasicConfigKey<T> implements ConfigKeySelfExtracting<T>, Serializab
         }
     }
     
-    private String name;
-    private TypeToken<T> typeToken;
-    private Class<? super T> type;
-    private String description;
-    private T defaultValue;
-    private boolean reconfigurable;
+    protected String name;
+    protected TypeToken<T> typeToken;
+    protected Class<? super T> type;
+    protected String description;
+    protected T defaultValue;
+    protected boolean reconfigurable;
+    protected ConfigInheritance typeInheritance;
+    protected ConfigInheritance parentInheritance;
+    protected Predicate<? super T> constraint;
+
+    /**
+     * Kept only for backwards compatibility with serialised state; when read, it's value is used 
+     * for {@link #parentInheritance} and then set to null.
+     */
+    @Deprecated
     private ConfigInheritance inheritance;
-    private Predicate<? super T> constraint;
 
     // FIXME In groovy, fields were `public final` with a default constructor; do we need the gson?
     public BasicConfigKey() { /* for gson */ }
@@ -171,14 +218,15 @@ public class BasicConfigKey<T> implements ConfigKeySelfExtracting<T>, Serializab
         this.constraint = Predicates.alwaysTrue();
     }
 
-    public BasicConfigKey(Builder<T> builder) {
+    public BasicConfigKey(Builder<T,?> builder) {
         this.name = checkNotNull(builder.name, "name");
         this.type = TypeTokens.getRawTypeIfRaw(checkNotNull(builder.type, "type"));
         this.typeToken = TypeTokens.getTypeTokenIfNotRaw(builder.type);
         this.description = builder.description;
         this.defaultValue = builder.defaultValue;
         this.reconfigurable = builder.reconfigurable;
-        this.inheritance = builder.inheritance;
+        this.parentInheritance = builder.parentInheritance;
+        this.typeInheritance = builder.typeInheritance;
         // Note: it's intentionally possible to have default values that are not valid
         // per the configured constraint. If validity were checked here any class that
         // contained a weirdly-defined config key would fail to initialise.
@@ -214,10 +262,23 @@ public class BasicConfigKey<T> implements ConfigKeySelfExtracting<T>, Serializab
         return reconfigurable;
     }
     
-    /** @see ConfigKey#getInheritance() */
-    @Override @Nullable
+    @Deprecated @Override @Nullable
     public ConfigInheritance getInheritance() {
-        return inheritance;
+        return getParentInheritance();
+    }
+
+    @Override @Nullable
+    public ConfigInheritance getTypeInheritance() {
+        return typeInheritance;
+    }
+
+    @Override @Nullable
+    public ConfigInheritance getParentInheritance() {
+        if (parentInheritance == null && inheritance != null) {
+            parentInheritance = inheritance;
+            inheritance = null;
+        }
+        return parentInheritance;
     }
 
     /** @see ConfigKey#getConstraint() */
@@ -306,7 +367,7 @@ public class BasicConfigKey<T> implements ConfigKeySelfExtracting<T>, Serializab
 
         /** builder here should be based on the same key passed in as parent */
         @Beta
-        public BasicConfigKeyOverwriting(Builder<T> builder, ConfigKey<T> parent) {
+        public BasicConfigKeyOverwriting(Builder<T,?> builder, ConfigKey<T> parent) {
             super(builder);
             parentKey = parent;
             Preconditions.checkArgument(Objects.equal(builder.name, parent.getName()), "Builder must use key of the same name.");

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/main/java/org/apache/brooklyn/core/config/ConfigKeys.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/config/ConfigKeys.java b/core/src/main/java/org/apache/brooklyn/core/config/ConfigKeys.java
index aeff698..f2bbc4a 100644
--- a/core/src/main/java/org/apache/brooklyn/core/config/ConfigKeys.java
+++ b/core/src/main/java/org/apache/brooklyn/core/config/ConfigKeys.java
@@ -126,16 +126,16 @@ public class ConfigKeys {
                 name, description, defaultValue);
     }
 
-    public static <T> BasicConfigKey.Builder<T> builder(Class<T> type) {
+    public static <T> BasicConfigKey.Builder<T,?> builder(Class<T> type) {
         return BasicConfigKey.builder(type);
     }
-    public static <T> BasicConfigKey.Builder<T> builder(TypeToken<T> type) {
+    public static <T> BasicConfigKey.Builder<T,?> builder(TypeToken<T> type) {
         return BasicConfigKey.builder(type);
     }
-    public static <T> BasicConfigKey.Builder<T> builder(Class<T> type, String name) {
+    public static <T> BasicConfigKey.Builder<T,?> builder(Class<T> type, String name) {
         return BasicConfigKey.builder(type, name);
     }
-    public static <T> BasicConfigKey.Builder<T> builder(TypeToken<T> type, String name) {
+    public static <T> BasicConfigKey.Builder<T,?> builder(TypeToken<T> type, String name) {
         return BasicConfigKey.builder(type, name);
     }
 

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/main/java/org/apache/brooklyn/core/config/MapConfigKey.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/config/MapConfigKey.java b/core/src/main/java/org/apache/brooklyn/core/config/MapConfigKey.java
index 96216ff..d06e160 100644
--- a/core/src/main/java/org/apache/brooklyn/core/config/MapConfigKey.java
+++ b/core/src/main/java/org/apache/brooklyn/core/config/MapConfigKey.java
@@ -18,6 +18,8 @@
  */
 package org.apache.brooklyn.core.config;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -35,6 +37,7 @@ import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Supplier;
 import com.google.common.collect.Maps;
+import com.google.common.reflect.TypeToken;
 
 /** A config key which represents a map, where contents can be accessed directly via subkeys.
  * Items added directly to the map must be of type map, and can be updated by:
@@ -49,11 +52,83 @@ import com.google.common.collect.Maps;
  * </ul>
  */
 //TODO Create interface
+//TODO The BasicConfigKey.builder(Class,name) methods mean we can't define them on this sub-type,
+//     because we want builder(Class<V>), which corresponds to BasicConfigKey<Map<String, V>.
 public class MapConfigKey<V> extends AbstractStructuredConfigKey<Map<String,V>,Map<String,Object>,V> {
     
     private static final long serialVersionUID = -6126481503795562602L;
     private static final Logger log = LoggerFactory.getLogger(MapConfigKey.class);
-    
+
+    public static <V> Builder<V> builder(MapConfigKey<V> key) {
+        return new Builder<V>(key);
+    }
+
+    public static class Builder<V> extends BasicConfigKey.Builder<Map<String, V>,Builder<V>> {
+        protected Class<V> subType;
+        
+        public Builder(TypeToken<V> subType, String name) {
+            super(new TypeToken<Map<String, V>>() {}, name);
+            this.subType = (Class<V>) subType.getRawType();
+        }
+        public Builder(Class<V> subType, String name) {
+            super(new TypeToken<Map<String, V>>() {}, name);
+            this.subType = checkNotNull(subType, "subType");
+        }
+        public Builder(MapConfigKey<V> key) {
+            super(checkNotNull(key.getTypeToken(), "type"), checkNotNull(key.getName(), "name"));
+            description(key.getDescription());
+            defaultValue(key.getDefaultValue());
+            reconfigurable(key.isReconfigurable());
+            parentInheritance(key.getParentInheritance());
+            typeInheritance(key.getTypeInheritance());
+            constraint(key.getConstraint());
+        }
+        public Builder<V> self() {
+            return this;
+        }
+        @Override
+        @Deprecated
+        public Builder<V> name(String val) {
+            throw new UnsupportedOperationException("Builder must be constructed with name");
+        }
+        @Override
+        @Deprecated
+        public Builder<V> type(Class<Map<String, V>> val) {
+            throw new UnsupportedOperationException("Builder must be constructed with type");
+        }
+        @Override
+        @Deprecated
+        public Builder<V> type(TypeToken<Map<String, V>> val) {
+            throw new UnsupportedOperationException("Builder must be constructed with type");
+        }
+        @Override
+        public MapConfigKey<V> build() {
+            return new MapConfigKey<V>(this);
+        }
+        
+        public String getName() {
+            return name;
+        }
+        public String getDescription() {
+            return description;
+        }
+    }
+
+    protected MapConfigKey(Builder<V> builder) {
+        super((Class)Map.class,
+                checkNotNull(builder.subType, "subType"),
+                checkNotNull(builder.name, "name"),
+                builder.description,
+                builder.defaultValue);
+        this.reconfigurable = builder.reconfigurable;
+        this.parentInheritance = builder.parentInheritance;
+        this.typeInheritance = builder.typeInheritance;
+        // Note: it's intentionally possible to have default values that are not valid
+        // per the configured constraint. If validity were checked here any class that
+        // contained a weirdly-defined config key would fail to initialise.
+        this.constraint = checkNotNull(builder.constraint, "constraint");
+    }
+
     public MapConfigKey(Class<V> subType, String name) {
         this(subType, name, name, null);
     }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractStructuredConfigKey.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractStructuredConfigKey.java b/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractStructuredConfigKey.java
index 8c7c610..532e1da 100644
--- a/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractStructuredConfigKey.java
+++ b/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractStructuredConfigKey.java
@@ -34,7 +34,7 @@ public abstract class AbstractStructuredConfigKey<T,RawT,V> extends BasicConfigK
 
     private static final long serialVersionUID = 7806267541029428561L;
 
-    public final Class<V> subType;
+    protected final Class<V> subType;
 
     public AbstractStructuredConfigKey(Class<T> type, Class<V> subType, String name, String description, T defaultValue) {
         super(type, name, description, defaultValue);

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java b/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java
index 607a22b..1526df0 100644
--- a/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java
@@ -24,6 +24,7 @@ import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKeyWithPrefix;
 import static org.apache.brooklyn.core.config.ConfigKeys.newStringConfigKey;
 
 import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.config.ConfigInheritance;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.config.MapConfigKey;
@@ -111,11 +112,11 @@ public class BrooklynConfigKeys {
     public static final ConfigKey<String> POST_LAUNCH_COMMAND = ConfigKeys.newStringConfigKey("post.launch.command",
             "Command to be run after the launch method being called on the driver");
 
-    public static final MapConfigKey<Object> SHELL_ENVIRONMENT = new MapConfigKey<Object>(
-            Object.class,
-            "shell.env", 
-            "Map of environment variables to pass to the runtime shell", 
-            ImmutableMap.<String,Object>of());
+    public static final MapConfigKey<Object> SHELL_ENVIRONMENT = new MapConfigKey.Builder<Object>(Object.class, "shell.env")
+            .description("Map of environment variables to pass to the runtime shell") 
+            .defaultValue(ImmutableMap.<String,Object>of())
+            .typeInheritance(ConfigInheritance.MERGE)
+            .build();
 
     public static final AttributeSensorAndConfigKey<String, String> INSTALL_DIR = new TemplatedStringAttributeSensorAndConfigKey("install.dir", "Directory for this software to be installed in",
             "${" +

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityConfigMap.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityConfigMap.java b/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityConfigMap.java
index da209e1..7337bbf 100644
--- a/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityConfigMap.java
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityConfigMap.java
@@ -21,7 +21,6 @@ package org.apache.brooklyn.core.entity.internal;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.apache.brooklyn.util.groovy.GroovyJavaMethods.elvis;
 
-import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -30,11 +29,14 @@ import java.util.Set;
 import org.apache.brooklyn.api.mgmt.ExecutionContext;
 import org.apache.brooklyn.api.mgmt.Task;
 import org.apache.brooklyn.config.ConfigInheritance;
+import org.apache.brooklyn.config.ConfigInheritance.InheritanceMode;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.Sanitizer;
 import org.apache.brooklyn.core.config.StructuredConfigKey;
 import org.apache.brooklyn.core.config.internal.AbstractConfigMapImpl;
 import org.apache.brooklyn.core.entity.AbstractEntity;
+import org.apache.brooklyn.core.location.AbstractLocation;
+import org.apache.brooklyn.util.collections.CollectionMerger;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.config.ConfigBag;
 import org.apache.brooklyn.util.core.flags.FlagUtils;
@@ -86,23 +88,20 @@ public class EntityConfigMap extends AbstractConfigMapImpl {
 
     @SuppressWarnings("unchecked")
     public <T> T getConfig(ConfigKey<T> key, T defaultValue) {
-        // FIXME What about inherited task in config?!
-        //              alex says: think that should work, no?
-        // FIXME What if someone calls getConfig on a task, before setting parent app?
-        //              alex says: not supported (throw exception, or return the task)
-        
         // In case this entity class has overridden the given key (e.g. to set default), then retrieve this entity's key
         // TODO If ask for a config value that's not in our configKeys, should we really continue with rest of method and return key.getDefaultValue?
         //      e.g. SshBasedJavaAppSetup calls setAttribute(JMX_USER), which calls getConfig(JMX_USER)
         //           but that example doesn't have a default...
         ConfigKey<T> ownKey = entity!=null ? (ConfigKey<T>)elvis(entity.getEntityType().getConfigKey(key.getName()), key) : key;
         
-        ConfigInheritance inheritance = key.getInheritance();
-        if (inheritance==null) inheritance = ownKey.getInheritance(); 
+        // TODO Confirm desired behaviour: I switched this around to get ownKey.getParentInheritance first.
+        ConfigInheritance inheritance = ownKey.getParentInheritance();
+        if (inheritance==null) inheritance = key.getInheritance(); 
         if (inheritance==null) {
             // TODO we could warn by introducing a temporary "ALWAYS_BUT_WARNING" instance
-            inheritance = getDefaultInheritance(); 
+            inheritance = getDefaultParentInheritance(); 
         }
+        InheritanceMode parentInheritanceMode = inheritance.isInherited(ownKey, entity.getParent(), entity);
         
         // TODO We're notifying of config-changed because currently persistence needs to know when the
         // attributeWhenReady is complete (so it can persist the result).
@@ -111,47 +110,107 @@ public class EntityConfigMap extends AbstractConfigMapImpl {
         // Don't use groovy truth: if the set value is e.g. 0, then would ignore set value and return default!
         if (ownKey instanceof ConfigKeySelfExtracting) {
             Object rawval = ownConfig.get(key);
-            T result = null;
-            boolean complete = false;
-            if (((ConfigKeySelfExtracting<T>)ownKey).isSet(ownConfig)) {
-                ExecutionContext exec = entity.getExecutionContext();
-                result = ((ConfigKeySelfExtracting<T>)ownKey).extractValue(ownConfig, exec);
-                complete = true;
-            } else if (isInherited(ownKey, inheritance) && 
-                    ((ConfigKeySelfExtracting<T>)ownKey).isSet(inheritedConfig)) {
-                ExecutionContext exec = entity.getExecutionContext();
-                result = ((ConfigKeySelfExtracting<T>)ownKey).extractValue(inheritedConfig, exec);
-                complete = true;
-            } else if (localConfigBag.containsKey(ownKey)) {
-                // TODO configBag.get doesn't handle tasks/attributeWhenReady - it only uses TypeCoercions
-                result = localConfigBag.get(ownKey);
-                complete = true;
-            } else if (isInherited(ownKey, inheritance) && 
-                    inheritedConfigBag.containsKey(ownKey)) {
-                result = inheritedConfigBag.get(ownKey);
-                complete = true;
-            }
+            Maybe<T> result = getConfigImpl((ConfigKeySelfExtracting<T>)ownKey, parentInheritanceMode);
 
             if (rawval instanceof Task) {
                 entity.getManagementSupport().getEntityChangeListener().onConfigChanged(key);
             }
-            if (complete) {
-                return result;
+            if (result.isPresent()) {
+                return result.get();
             }
         } else {
             LOG.warn("Config key {} of {} is not a ConfigKeySelfExtracting; cannot retrieve value; returning default", ownKey, this);
         }
         return TypeCoercions.coerce((defaultValue != null) ? defaultValue : ownKey.getDefaultValue(), key.getTypeToken());
     }
+    
+    @SuppressWarnings("unchecked")
+    private <T> Maybe<T> getConfigImpl(ConfigKeySelfExtracting<T> key, InheritanceMode parentInheritance) {
+        ExecutionContext exec = entity.getExecutionContext();
+        Maybe<T> ownValue;
+        Maybe<T> parentValue;
+
+        // Get own value
+        if (((ConfigKeySelfExtracting<T>)key).isSet(ownConfig)) {
+            ownValue = Maybe.of(((ConfigKeySelfExtracting<T>)key).extractValue(ownConfig, exec));
+        } else if (localConfigBag.containsKey(key)) {
+            // TODO configBag.get doesn't handle tasks/attributeWhenReady - it only uses TypeCoercions
+            // Precedence ordering has changed; previously we'd prefer an explicit isSet(inheritedConfig)
+            // over the localConfigBag.get(key).
+            ownValue = Maybe.of(localConfigBag.get(key));
+        } else {
+            ownValue = Maybe.<T>absent();
+        }
+        
+        // Get the parent-inheritance value (but only if we'll need it)
+        switch (parentInheritance) {
+        case IF_NO_EXPLICIT_VALUE:
+            if (ownValue.isAbsent()) {
+                if (((ConfigKeySelfExtracting<T>)key).isSet(inheritedConfig)) {
+                    parentValue = Maybe.of(((ConfigKeySelfExtracting<T>)key).extractValue(inheritedConfig, exec));
+                } else if (inheritedConfigBag.containsKey(key)) {
+                    parentValue = Maybe.of(inheritedConfigBag.get(key));
+                } else {
+                    parentValue = Maybe.absent();
+                }
+            } else {
+                parentValue = Maybe.absent();
+            }
+            break;
+        case MERGE:
+            if (((ConfigKeySelfExtracting<T>)key).isSet(inheritedConfig)) {
+                parentValue = Maybe.of(((ConfigKeySelfExtracting<T>)key).extractValue(inheritedConfig, exec));
+            } else if (inheritedConfigBag.containsKey(key)) {
+                parentValue = Maybe.of(inheritedConfigBag.get(key));
+            } else {
+                parentValue = Maybe.absent();
+            }
+            break;
+        case NONE:
+            parentValue = Maybe.absent();
+            break;
+        default:
+            throw new IllegalStateException("Unsupported parent-inheritance mode for "+key.getName()+": "+parentInheritance);
+        }
+
+        // Merge or override, as appropriate
+        switch (parentInheritance) {
+        case IF_NO_EXPLICIT_VALUE:
+            return ownValue.isPresent() ? ownValue : parentValue;
+        case MERGE:
+            return (Maybe<T>) deepMerge(ownValue, parentValue, key);
+        case NONE:
+            return ownValue;
+        default:
+            throw new IllegalStateException("Unsupported parent-inheritance mode for "+key.getName()+": "+parentInheritance);
+        }
+    }
+    
+    private <T> Maybe<?> deepMerge(Maybe<? extends T> val1, Maybe<? extends T> val2, ConfigKey<?> keyForLogging) {
+        if (val2.isAbsent() || val2.isNull()) {
+            return val1;
+        } else if (val1.isAbsent()) {
+            return val2;
+        } else if (val1.isNull()) {
+            return val1; // an explicit null means an override; don't merge
+        } else if (val1.get() instanceof Map && val2.get() instanceof Map) {
+            return Maybe.of(CollectionMerger.builder().build().merge((Map<?,?>)val1.get(), (Map<?,?>)val2.get()));
+        } else {
+            // cannot merge; just return val1
+            LOG.debug("Cannot merge values for "+keyForLogging.getName()+", because values are not maps: "+val1.get().getClass()+", and "+val2.get().getClass());
+            return val1;
+        }
+    }
 
     private <T> boolean isInherited(ConfigKey<T> key) {
-        return isInherited(key, key.getInheritance());
+        return isInherited(key, key.getParentInheritance());
     }
     private <T> boolean isInherited(ConfigKey<T> key, ConfigInheritance inheritance) {
-        if (inheritance==null) inheritance = getDefaultInheritance(); 
-        return inheritance.isInherited(key, entity.getParent(), entity);
+        if (inheritance==null) inheritance = getDefaultParentInheritance();
+        InheritanceMode mode = inheritance.isInherited(key, entity.getParent(), entity);
+        return mode != null && mode != InheritanceMode.NONE;
     }
-    private ConfigInheritance getDefaultInheritance() {
+    private ConfigInheritance getDefaultParentInheritance() {
         return ConfigInheritance.ALWAYS; 
     }
 

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/ServiceStateLogic.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/ServiceStateLogic.java b/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/ServiceStateLogic.java
index c8f8400..33eb3bf 100644
--- a/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/ServiceStateLogic.java
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/ServiceStateLogic.java
@@ -387,12 +387,12 @@ public class ServiceStateLogic {
         public static final ConfigKey<QuorumCheck> UP_QUORUM_CHECK = ConfigKeys.builder(QuorumCheck.class, "enricher.service_state.children_and_members.quorum.up")
             .description("Logic for checking whether this service is up, based on children and/or members, defaulting to allowing none but if there are any requiring at least one to be up")
             .defaultValue(QuorumCheck.QuorumChecks.atLeastOneUnlessEmpty())
-            .inheritance(ConfigInheritance.NONE)
+            .parentInheritance(ConfigInheritance.NONE)
             .build();
         public static final ConfigKey<QuorumCheck> RUNNING_QUORUM_CHECK = ConfigKeys.builder(QuorumCheck.class, "enricher.service_state.children_and_members.quorum.running") 
             .description("Logic for checking whether this service is healthy, based on children and/or members running, defaulting to requiring none to be ON-FIRE")
             .defaultValue(QuorumCheck.QuorumChecks.all())
-            .inheritance(ConfigInheritance.NONE)
+            .parentInheritance(ConfigInheritance.NONE)
             .build();
         // TODO items below should probably also have inheritance NONE ?
         public static final ConfigKey<Boolean> DERIVE_SERVICE_NOT_UP = ConfigKeys.newBooleanConfigKey("enricher.service_state.children_and_members.service_up.publish", "Whether to derive a service-not-up indicator from children", true);

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/main/java/org/apache/brooklyn/core/location/AbstractLocation.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/location/AbstractLocation.java b/core/src/main/java/org/apache/brooklyn/core/location/AbstractLocation.java
index d4e565e..1ca8c2f 100644
--- a/core/src/main/java/org/apache/brooklyn/core/location/AbstractLocation.java
+++ b/core/src/main/java/org/apache/brooklyn/core/location/AbstractLocation.java
@@ -44,6 +44,7 @@ import org.apache.brooklyn.api.objs.Configurable;
 import org.apache.brooklyn.api.sensor.Sensor;
 import org.apache.brooklyn.api.sensor.SensorEventListener;
 import org.apache.brooklyn.config.ConfigInheritance;
+import org.apache.brooklyn.config.ConfigInheritance.InheritanceMode;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
 import org.apache.brooklyn.core.BrooklynFeatureEnablement;
@@ -488,7 +489,8 @@ public abstract class AbstractLocation extends AbstractBrooklynObject implements
         private boolean isInherited(ConfigKey<?> key) {
             ConfigInheritance inheritance = key.getInheritance();
             if (inheritance==null) inheritance = getDefaultInheritance();
-            return inheritance.isInherited(key, getParent(), AbstractLocation.this);
+            InheritanceMode mode = inheritance.isInherited(key, getParent(), AbstractLocation.this);
+            return mode != null && mode != InheritanceMode.NONE;
         }
 
         private ConfigInheritance getDefaultInheritance() {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/main/java/org/apache/brooklyn/core/objs/BasicSpecParameter.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/objs/BasicSpecParameter.java b/core/src/main/java/org/apache/brooklyn/core/objs/BasicSpecParameter.java
index 89b7cd0..d89c8cb 100644
--- a/core/src/main/java/org/apache/brooklyn/core/objs/BasicSpecParameter.java
+++ b/core/src/main/java/org/apache/brooklyn/core/objs/BasicSpecParameter.java
@@ -37,6 +37,7 @@ import org.apache.brooklyn.api.objs.BrooklynObject;
 import org.apache.brooklyn.api.objs.BrooklynType;
 import org.apache.brooklyn.api.objs.SpecParameter;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigInheritance;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
 import org.apache.brooklyn.core.config.BasicConfigKey;
@@ -227,6 +228,8 @@ public class BasicSpecParameter<T> implements SpecParameter<T>{
             String type = (String)inputDef.get("type");
             Object defaultValue = inputDef.get("default");
             Predicate<?> constraints = parseConstraints(inputDef.get("constraints"), loader);
+            ConfigInheritance parentInheritance = parseInheritance(inputDef.get("inheritance.parent"), loader);
+            ConfigInheritance typeInheritance = parseInheritance(inputDef.get("inheritance.type"), loader);
 
             if (name == null) {
                 throw new IllegalArgumentException("'name' value missing from input definition " + obj + " but is required. Check for typos.");
@@ -240,7 +243,9 @@ public class BasicSpecParameter<T> implements SpecParameter<T>{
                 .name(name)
                 .description(description)
                 .defaultValue(defaultValue)
-                .constraint(constraints);
+                .constraint(constraints)
+                .parentInheritance(parentInheritance)
+                .typeInheritance(typeInheritance);
             
             if (PortRange.class.equals(typeToken.getRawType())) {
                 sensorType = new PortAttributeSensorAndConfigKey(builder);
@@ -299,6 +304,14 @@ public class BasicSpecParameter<T> implements SpecParameter<T>{
                 return Predicates.alwaysTrue();
             }
         }
+        
+        private static ConfigInheritance parseInheritance(Object obj, BrooklynClassLoadingContext loader) {
+            if (obj == null || obj instanceof String) {
+                return ConfigInheritance.fromString((String)obj);
+            } else {
+                throw new IllegalArgumentException ("The config-inheritance '" + obj + "' for a catalog input is invalid format - string supported");
+            }
+        }
     }
 
     private static final class ParseClassParameters {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/main/java/org/apache/brooklyn/core/sensor/AttributeSensorAndConfigKey.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/sensor/AttributeSensorAndConfigKey.java b/core/src/main/java/org/apache/brooklyn/core/sensor/AttributeSensorAndConfigKey.java
index 2a26fa7..0580a77 100644
--- a/core/src/main/java/org/apache/brooklyn/core/sensor/AttributeSensorAndConfigKey.java
+++ b/core/src/main/java/org/apache/brooklyn/core/sensor/AttributeSensorAndConfigKey.java
@@ -88,7 +88,7 @@ public abstract class AttributeSensorAndConfigKey<ConfigType,SensorType> extends
         configKey = ConfigKeys.newConfigKeyWithDefault(orig.configKey, 
                 TypeCoercions.coerce(defaultValue, orig.configKey.getTypeToken()));
     }
-    public AttributeSensorAndConfigKey(Builder<ConfigType> configKeyBuilder, TypeToken<SensorType> sensorType) {
+    public AttributeSensorAndConfigKey(Builder<ConfigType,?> configKeyBuilder, TypeToken<SensorType> sensorType) {
         super(sensorType, configKeyBuilder.getName(), configKeyBuilder.getDescription());
         configKey = new BasicConfigKey<ConfigType>(configKeyBuilder);
     }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/main/java/org/apache/brooklyn/core/sensor/PortAttributeSensorAndConfigKey.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/sensor/PortAttributeSensorAndConfigKey.java b/core/src/main/java/org/apache/brooklyn/core/sensor/PortAttributeSensorAndConfigKey.java
index aa396d2..556e7b0 100644
--- a/core/src/main/java/org/apache/brooklyn/core/sensor/PortAttributeSensorAndConfigKey.java
+++ b/core/src/main/java/org/apache/brooklyn/core/sensor/PortAttributeSensorAndConfigKey.java
@@ -70,7 +70,7 @@ public class PortAttributeSensorAndConfigKey extends AttributeSensorAndConfigKey
     public PortAttributeSensorAndConfigKey(PortAttributeSensorAndConfigKey orig, Object defaultValue) {
         super(orig, TypeCoercions.coerce(defaultValue, PortRange.class));
     }
-    public PortAttributeSensorAndConfigKey(BasicConfigKey.Builder<PortRange> builder) {
+    public PortAttributeSensorAndConfigKey(BasicConfigKey.Builder<PortRange,?> builder) {
         super(builder, TypeToken.of(Integer.class));
     }
     

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/test/java/org/apache/brooklyn/core/config/ConfigKeysTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/config/ConfigKeysTest.java b/core/src/test/java/org/apache/brooklyn/core/config/ConfigKeysTest.java
index bb5a323..9e82c1a 100644
--- a/core/src/test/java/org/apache/brooklyn/core/config/ConfigKeysTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/config/ConfigKeysTest.java
@@ -82,7 +82,7 @@ public class ConfigKeysTest {
         ConfigKey<String> key = ConfigKeys.builder(String.class, "mykey")
             .description("my descr")
             .defaultValue("my default val")
-            .inheritance(ConfigInheritance.NONE)
+            .parentInheritance(ConfigInheritance.NONE)
             .reconfigurable(true)
             .build();
         
@@ -98,7 +98,7 @@ public class ConfigKeysTest {
         assertEquals(key.getDescription(), "my descr");
         assertEquals(key.getDefaultValue(), "my default val");
         assertEquals(key.isReconfigurable(), true);
-        assertEquals(key.getInheritance(), ConfigInheritance.NONE);
+        assertEquals(key.getParentInheritance(), ConfigInheritance.NONE);
     }
 
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/core/src/test/java/org/apache/brooklyn/core/entity/ConfigEntityInheritanceTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/entity/ConfigEntityInheritanceTest.java b/core/src/test/java/org/apache/brooklyn/core/entity/ConfigEntityInheritanceTest.java
index a6b1602..3261d6f 100644
--- a/core/src/test/java/org/apache/brooklyn/core/entity/ConfigEntityInheritanceTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/entity/ConfigEntityInheritanceTest.java
@@ -167,9 +167,9 @@ public class ConfigEntityInheritanceTest extends BrooklynAppUnitTestSupport {
     
     public static class MyEntityWithPartiallyHeritableConfig extends AbstractEntity {
         public static final ConfigKey<String> HERITABLE = ConfigKeys.builder(String.class, "herit.default").build();
-        public static final ConfigKey<String> UNINHERITABLE = ConfigKeys.builder(String.class, "herit.none").inheritance(ConfigInheritance.NONE).build();
+        public static final ConfigKey<String> UNINHERITABLE = ConfigKeys.builder(String.class, "herit.none").parentInheritance(ConfigInheritance.NONE).build();
         // i find a strange joy in words where the prefix "in-" does not mean not, like inflammable 
-        public static final ConfigKey<String> ALWAYS_HERITABLE = ConfigKeys.builder(String.class, "herit.always").inheritance(ConfigInheritance.ALWAYS).build();
+        public static final ConfigKey<String> ALWAYS_HERITABLE = ConfigKeys.builder(String.class, "herit.always").parentInheritance(ConfigInheritance.ALWAYS).build();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java
index 7ee393a..304b6c7 100644
--- a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java
@@ -22,12 +22,14 @@ import java.util.Collection;
 import java.util.Map;
 
 import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.reflect.TypeToken;
 
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.location.MachineProvisioningLocation;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigInheritance;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.annotation.Effector;
 import org.apache.brooklyn.core.config.ConfigKeys;
@@ -39,7 +41,6 @@ import org.apache.brooklyn.core.entity.lifecycle.Lifecycle.Transition;
 import org.apache.brooklyn.core.entity.trait.Startable;
 import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
 import org.apache.brooklyn.core.sensor.Sensors;
-import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.flags.SetFromFlag;
 import org.apache.brooklyn.util.time.Duration;
 
@@ -50,6 +51,8 @@ public interface SoftwareProcess extends Entity, Startable {
     AttributeSensor<String> SUBNET_HOSTNAME = Attributes.SUBNET_HOSTNAME;
     AttributeSensor<String> SUBNET_ADDRESS = Attributes.SUBNET_ADDRESS;
 
+    // TODO Want this to have typeInheritance.merge as well, but currently only supported for maps
+    @SuppressWarnings("serial")
     ConfigKey<Collection<Integer>> REQUIRED_OPEN_LOGIN_PORTS = ConfigKeys.newConfigKey(
             new TypeToken<Collection<Integer>>() {},
             "requiredOpenLoginPorts",
@@ -169,10 +172,11 @@ public interface SoftwareProcess extends Entity, Startable {
      * @see #PRE_INSTALL_TEMPLATES
      */
     @Beta
-    @SuppressWarnings("serial")
     @SetFromFlag("preInstallFiles")
-    ConfigKey<Map<String, String>> PRE_INSTALL_FILES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
-            "files.preinstall", "Mapping of files, to be copied before install, to destination name relative to installDir");
+    MapConfigKey<String> PRE_INSTALL_FILES = new MapConfigKey.Builder<String>(String.class, "files.preinstall")
+            .description("Mapping of files, to be copied before install, to destination name relative to installDir") 
+            .typeInheritance(ConfigInheritance.MERGE)
+            .build();
 
     /**
      * Templates to be filled in and then copied to the server before install.
@@ -180,10 +184,11 @@ public interface SoftwareProcess extends Entity, Startable {
      * @see #PRE_INSTALL_FILES
      */
     @Beta
-    @SuppressWarnings("serial")
     @SetFromFlag("preInstallTemplates")
-    ConfigKey<Map<String, String>> PRE_INSTALL_TEMPLATES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
-            "templates.preinstall", "Mapping of templates, to be filled in and copied before pre-install, to destination name relative to installDir");
+    MapConfigKey<String> PRE_INSTALL_TEMPLATES = new MapConfigKey.Builder<String>(String.class, "templates.preinstall")
+            .description("Mapping of templates, to be filled in and copied before pre-install, to destination name relative to installDir") 
+            .typeInheritance(ConfigInheritance.MERGE)
+            .build();
 
     /**
      * Files to be copied to the server before install.
@@ -194,10 +199,11 @@ public interface SoftwareProcess extends Entity, Startable {
      * @see #INSTALL_TEMPLATES
      */
     @Beta
-    @SuppressWarnings("serial")
     @SetFromFlag("installFiles")
-    ConfigKey<Map<String, String>> INSTALL_FILES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
-            "files.install", "Mapping of files, to be copied before install, to destination name relative to installDir");
+    MapConfigKey<String> INSTALL_FILES = new MapConfigKey.Builder<String>(String.class, "files.install")
+            .description("Mapping of files, to be copied before install, to destination name relative to installDir") 
+            .typeInheritance(ConfigInheritance.MERGE)
+            .build();
 
     /**
      * Templates to be filled in and then copied to the server before install.
@@ -205,10 +211,11 @@ public interface SoftwareProcess extends Entity, Startable {
      * @see #INSTALL_FILES
      */
     @Beta
-    @SuppressWarnings("serial")
     @SetFromFlag("installTemplates")
-    ConfigKey<Map<String, String>> INSTALL_TEMPLATES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
-            "templates.install", "Mapping of templates, to be filled in and copied before install, to destination name relative to installDir");
+    MapConfigKey<String> INSTALL_TEMPLATES = new MapConfigKey.Builder<String>(String.class, "templates.install")
+            .description("Mapping of templates, to be filled in and copied before install, to destination name relative to installDir") 
+            .typeInheritance(ConfigInheritance.MERGE)
+            .build();
 
     /**
      * Files to be copied to the server after customisation.
@@ -219,10 +226,11 @@ public interface SoftwareProcess extends Entity, Startable {
      * @see #RUNTIME_TEMPLATES
      */
     @Beta
-    @SuppressWarnings("serial")
     @SetFromFlag("runtimeFiles")
-    ConfigKey<Map<String, String>> RUNTIME_FILES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
-            "files.runtime", "Mapping of files, to be copied before customisation, to destination name relative to runDir");
+    MapConfigKey<String> RUNTIME_FILES = new MapConfigKey.Builder<String>(String.class, "files.runtime")
+            .description("Mapping of files, to be copied before customisation, to destination name relative to runDir") 
+            .typeInheritance(ConfigInheritance.MERGE)
+            .build();
 
     /**
      * Templates to be filled in and then copied to the server after customisation.
@@ -230,14 +238,18 @@ public interface SoftwareProcess extends Entity, Startable {
      * @see #RUNTIME_FILES
      */
     @Beta
-    @SuppressWarnings("serial")
     @SetFromFlag("runtimeTemplates")
-    ConfigKey<Map<String, String>> RUNTIME_TEMPLATES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
-            "templates.runtime", "Mapping of templates, to be filled in and copied before customisation, to destination name relative to runDir");
+    MapConfigKey<String> RUNTIME_TEMPLATES = new MapConfigKey.Builder<String>(String.class, "templates.runtime")
+            .description("Mapping of templates, to be filled in and copied before customisation, to destination name relative to runDir") 
+            .typeInheritance(ConfigInheritance.MERGE)
+            .build();
 
     @SetFromFlag("provisioningProperties")
-    MapConfigKey<Object> PROVISIONING_PROPERTIES = new MapConfigKey<Object>(Object.class,
-            "provisioning.properties", "Custom properties to be passed in when provisioning a new machine", MutableMap.<String,Object>of());
+    MapConfigKey<Object> PROVISIONING_PROPERTIES = new MapConfigKey.Builder<Object>(Object.class, "provisioning.properties")
+            .description("Custom properties to be passed in when provisioning a new machine")
+            .defaultValue(ImmutableMap.<String, Object>of())
+            .typeInheritance(ConfigInheritance.MERGE)
+            .build();
 
     @SetFromFlag("maxRebindSensorsDelay")
     ConfigKey<Duration> MAXIMUM_REBIND_SENSOR_CONNECT_DELAY = ConfigKeys.newConfigKey(Duration.class,

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/utils/common/src/main/java/org/apache/brooklyn/config/ConfigInheritance.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/config/ConfigInheritance.java b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigInheritance.java
index 4d581eb..81db3cc 100644
--- a/utils/common/src/main/java/org/apache/brooklyn/config/ConfigInheritance.java
+++ b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigInheritance.java
@@ -20,31 +20,60 @@ package org.apache.brooklyn.config;
 
 import java.io.Serializable;
 
+import org.apache.brooklyn.util.text.Strings;
+
 import com.google.common.annotations.Beta;
 
 @SuppressWarnings("serial")
 public abstract class ConfigInheritance implements Serializable {
 
-    public static final ConfigInheritance ALWAYS = new Always();
+    public enum InheritanceMode {
+        NONE,
+        IF_NO_EXPLICIT_VALUE,
+        MERGE
+    }
+
     public static final ConfigInheritance NONE = new None();
+    public static final ConfigInheritance ALWAYS = new Always();
+    public static final ConfigInheritance MERGE = new Merged();
+    
+    public static ConfigInheritance fromString(String val) {
+        if (Strings.isBlank(val)) return null;
+        switch (val.toLowerCase().trim()) {
+        case "none":
+            return NONE;
+        case "always": 
+            return ALWAYS;
+        case "merge" :
+            return MERGE;
+        default:
+            throw new IllegalArgumentException("Invalid config-inheritance '"+val+"' (legal values are none, always or merge)");
+        }
+    }
     
     private ConfigInheritance() {}
     
     @Beta
-    public abstract boolean isInherited(ConfigKey<?> key, Object from, Object to);
+    public abstract InheritanceMode isInherited(ConfigKey<?> key, Object from, Object to);
 
     private static class Always extends ConfigInheritance {
         @Override
-        public boolean isInherited(ConfigKey<?> key, Object from, Object to) {
-            return true;
+        public InheritanceMode isInherited(ConfigKey<?> key, Object from, Object to) {
+            return InheritanceMode.IF_NO_EXPLICIT_VALUE;
         }
     }
 
     private static class None extends ConfigInheritance {
         @Override
-        public boolean isInherited(ConfigKey<?> key, Object from, Object to) {
-            return false;
+        public InheritanceMode isInherited(ConfigKey<?> key, Object from, Object to) {
+            return InheritanceMode.NONE;
         }
     }
     
+    private static class Merged extends ConfigInheritance {
+        @Override
+        public InheritanceMode isInherited(ConfigKey<?> key, Object from, Object to) {
+            return InheritanceMode.MERGE;
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/57beafa2/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java
index 92f174f..3b66e98 100644
--- a/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java
+++ b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java
@@ -84,8 +84,20 @@ public interface ConfigKey<T> {
     boolean isReconfigurable();
 
     /**
+     * @return The sub-typing inheritance model, or <code>null</code> for the default in any context.
+     */
+    @Nullable ConfigInheritance getTypeInheritance();
+
+    /**
+     * @return The inheritance-from-parent-entities model, or <code>null</code> for the default in any context.
+     */
+    @Nullable ConfigInheritance getParentInheritance();
+
+    /**
      * @return The inheritance model, or <code>null</code> for the default in any context.
+     * @deprecated since 0.10.0; use {@link #getParentInheritance()}.
      */
+    @Deprecated
     @Nullable ConfigInheritance getInheritance();
 
     /**


Mime
View raw message