Closes #229

Merge brooklyn.parameters in YAML files

Implements behaviour required to merge `brooklyn.parameters` sections of specifications in YAML files, allowing addition of new parameters to entities without having to specify the complete list again.
diff --git a/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshCommandEffector.java b/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshCommandEffector.java
index ca952fe..957d68e 100644
--- a/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshCommandEffector.java
+++ b/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshCommandEffector.java
@@ -19,34 +19,39 @@
 package org.apache.brooklyn.core.effector.ssh;
 
 import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Maps;
 
 import org.apache.brooklyn.api.effector.Effector;
 import org.apache.brooklyn.api.effector.ParameterType;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.config.MapConfigKey;
 import org.apache.brooklyn.core.effector.AddEffector;
 import org.apache.brooklyn.core.effector.EffectorBody;
 import org.apache.brooklyn.core.effector.Effectors;
 import org.apache.brooklyn.core.effector.Effectors.EffectorBuilder;
 import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
-import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.sensor.ssh.SshCommandSensor;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.config.ConfigBag;
 import org.apache.brooklyn.util.core.json.ShellEnvironmentSerializer;
-import org.apache.brooklyn.util.text.Strings;
-
-import com.google.common.base.Preconditions;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.exceptions.Exceptions;
 
 public final class SshCommandEffector extends AddEffector {
-    
+
     public static final ConfigKey<String> EFFECTOR_COMMAND = ConfigKeys.newStringConfigKey("command");
     public static final ConfigKey<String> EFFECTOR_EXECUTION_DIR = SshCommandSensor.SENSOR_EXECUTION_DIR;
-    
+    public static final MapConfigKey<Object> EFFECTOR_SHELL_ENVIRONMENT = BrooklynConfigKeys.SHELL_ENVIRONMENT;
+
     public SshCommandEffector(ConfigBag params) {
         super(newEffectorBuilder(params).build());
     }
-    
+
     public SshCommandEffector(Map<String,String> params) {
         this(ConfigBag.newInstance(params));
     }
@@ -57,7 +62,6 @@
         return eff;
     }
 
-
     protected static class Body extends EffectorBody<String> {
         private final Effector<?> effector;
         private final String command;
@@ -65,42 +69,48 @@
 
         public Body(Effector<?> eff, ConfigBag params) {
             this.effector = eff;
-            this.command = Preconditions.checkNotNull(params.get(EFFECTOR_COMMAND), "command must be supplied when defining this effector");
+            this.command = Preconditions.checkNotNull(params.get(EFFECTOR_COMMAND), "SSH command must be supplied when defining this effector");
             this.executionDir = params.get(EFFECTOR_EXECUTION_DIR);
-            // TODO could take a custom "env" aka effectorShellEnv
         }
 
         @Override
         public String call(ConfigBag params) {
-            String command = this.command;
-            
-            command = SshCommandSensor.makeCommandExecutingInDirectory(command, executionDir, entity());
-            
-            MutableMap<String, String> env = MutableMap.of();
-            // first set all declared parameters, including default values
-            for (ParameterType<?> param: effector.getParameters()) {
-                env.addIfNotNull(param.getName(), Strings.toString( params.get(Effectors.asConfigKey(param)) ));
+            String sshCommand = SshCommandSensor.makeCommandExecutingInDirectory(command, executionDir, entity());
+
+            MutableMap<String, Object> env = MutableMap.of();
+
+            // Set all declared parameters, including default values
+            for (ParameterType<?> param : effector.getParameters()) {
+                env.addIfNotNull(param.getName(), params.get(Effectors.asConfigKey(param)));
             }
-            
-            // then set things from the entities defined shell environment, if applicable
-            Map<String, Object> shellEnv = entity().getConfig(BrooklynConfigKeys.SHELL_ENVIRONMENT);
-            ShellEnvironmentSerializer envSerializer = new ShellEnvironmentSerializer(((EntityInternal)entity()).getManagementContext());
-            env.putAll(envSerializer.serialize(shellEnv));
-            
-            // if we wanted to resolve the surrounding environment in real time -- see above
-//            Map<String,Object> paramsResolved = (Map<String, Object>) Tasks.resolveDeepValue(effectorShellEnv, Map.class, entity().getExecutionContext());
-            
-            // finally set the parameters we've been passed; this will repeat declared parameters but to no harm,
-            // it may pick up additional values (could be a flag defining whether this is permitted or not)
-            env.putAll(Strings.toStringMap(params.getAllConfig()));
-            
-            SshEffectorTasks.SshEffectorTaskFactory<String> t = SshEffectorTasks.ssh(command)
-                .requiringZeroAndReturningStdout()
-                .summary("effector "+effector.getName())
-                .environmentVariables(env);
-            return queue(t).get();
+
+            // Set things from the entities defined shell environment, if applicable
+            env.putAll(entity().config().get(BrooklynConfigKeys.SHELL_ENVIRONMENT));
+
+            // Add the shell environment entries from our configuration
+            Map<String, Object> effectorEnv = params.get(EFFECTOR_SHELL_ENVIRONMENT);
+            if (effectorEnv != null) env.putAll(effectorEnv);
+
+            // Set the parameters we've been passed. This will repeat declared parameters but to no harm,
+            // it may pick up additional values (could be a flag defining whether this is permitted or not.)
+            // Make sure we do not include the shell.env here again, by filtering it out.
+            env.putAll(Maps.filterKeys(params.getAllConfig(), Predicates.not(Predicates.equalTo(EFFECTOR_SHELL_ENVIRONMENT.getName()))));
+
+            // Try to resolve the configuration in the env Map
+            try {
+                env = (MutableMap<String, Object>) Tasks.resolveDeepValue(env, Object.class, entity().getExecutionContext());
+            } catch (InterruptedException | ExecutionException e) {
+                Exceptions.propagateIfFatal(e);
+            }
+
+            // Execute the effector with the serialized environment strings
+            ShellEnvironmentSerializer serializer = new ShellEnvironmentSerializer(entity().getManagementContext());
+            SshEffectorTasks.SshEffectorTaskFactory<String> task = SshEffectorTasks.ssh(sshCommand)
+                    .requiringZeroAndReturningStdout()
+                    .summary("effector "+effector.getName())
+                    .environmentVariables(serializer.serialize(env));
+
+            return queue(task).get();
         }
-        
     }
-    
 }
diff --git a/core/src/main/java/org/apache/brooklyn/core/sensor/ssh/SshCommandSensor.java b/core/src/main/java/org/apache/brooklyn/core/sensor/ssh/SshCommandSensor.java
index 3464281..8b1d410 100644
--- a/core/src/main/java/org/apache/brooklyn/core/sensor/ssh/SshCommandSensor.java
+++ b/core/src/main/java/org/apache/brooklyn/core/sensor/ssh/SshCommandSensor.java
@@ -19,6 +19,7 @@
 package org.apache.brooklyn.core.sensor.ssh;
 
 import java.util.Map;
+import java.util.concurrent.ExecutionException;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -33,6 +34,7 @@
 import org.apache.brooklyn.api.entity.EntityLocal;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.config.MapConfigKey;
 import org.apache.brooklyn.core.effector.AddSensor;
 import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
 import org.apache.brooklyn.core.entity.EntityInternal;
@@ -44,6 +46,8 @@
 import org.apache.brooklyn.util.core.config.ConfigBag;
 import org.apache.brooklyn.util.core.flags.TypeCoercions;
 import org.apache.brooklyn.util.core.json.ShellEnvironmentSerializer;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.os.Os;
 import org.apache.brooklyn.util.text.Strings;
 
@@ -63,17 +67,19 @@
     public static final ConfigKey<String> SENSOR_EXECUTION_DIR = ConfigKeys.newStringConfigKey("executionDir", "Directory where the command should run; "
         + "if not supplied, executes in the entity's run dir (or home dir if no run dir is defined); "
         + "use '~' to always execute in the home dir, or 'custom-feed/' to execute in a custom-feed dir relative to the run dir");
+    public static final MapConfigKey<Object> SENSOR_SHELL_ENVIRONMENT = BrooklynConfigKeys.SHELL_ENVIRONMENT;
 
     protected final String command;
     protected final String executionDir;
+    protected final Map<String,Object> sensorEnv;
 
     public SshCommandSensor(final ConfigBag params) {
         super(params);
 
         // TODO create a supplier for the command string to support attribute embedding
-        command = Preconditions.checkNotNull(params.get(SENSOR_COMMAND), "command");
-
+        command = Preconditions.checkNotNull(params.get(SENSOR_COMMAND), "SSH command must be dupplied when defining this sensor");
         executionDir = params.get(SENSOR_EXECUTION_DIR);
+        sensorEnv = params.get(SENSOR_SHELL_ENVIRONMENT);
     }
 
     @Override
@@ -87,9 +93,21 @@
         Supplier<Map<String,String>> envSupplier = new Supplier<Map<String,String>>() {
             @Override
             public Map<String, String> get() {
-                Map<String, Object> env = entity.getConfig(BrooklynConfigKeys.SHELL_ENVIRONMENT);
-                ShellEnvironmentSerializer envSerializer = new ShellEnvironmentSerializer(((EntityInternal)entity).getManagementContext());
-                return envSerializer.serialize(env);
+                Map<String, Object> env = MutableMap.copyOf(entity.getConfig(BrooklynConfigKeys.SHELL_ENVIRONMENT));
+
+                // Add the shell environment entries from our configuration
+                if (sensorEnv != null) env.putAll(sensorEnv);
+
+                // Try to resolve the configuration in the env Map
+                try {
+                    env = (Map<String, Object>) Tasks.resolveDeepValue(env, Object.class, ((EntityInternal) entity).getExecutionContext());
+                } catch (InterruptedException | ExecutionException e) {
+                    Exceptions.propagateIfFatal(e);
+                }
+
+                // Convert the environment into strings with the serializer
+                ShellEnvironmentSerializer serializer = new ShellEnvironmentSerializer(((EntityInternal) entity).getManagementContext());
+                return serializer.serialize(env);
             }
         };
 
@@ -109,7 +127,7 @@
                 .onSuccess(Functions.compose(new Function<String, T>() {
                         @Override
                         public T apply(String input) {
-                            return TypeCoercions.coerce(Strings.trimEnd(input), getType(entity, type));
+                            return TypeCoercions.coerce(Strings.trimEnd(input), (Class<T>) sensor.getType());
                         }}, SshValueFunctions.stdout()));
 
         SshFeed.builder()