brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aleds...@apache.org
Subject [1/5] brooklyn-server git commit: Adds OnSubnetNetworkEnricher
Date Fri, 10 Jun 2016 14:02:37 GMT
Repository: brooklyn-server
Updated Branches:
  refs/heads/master f763b3e46 -> 2f23bd2be


Adds OnSubnetNetworkEnricher

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

Branch: refs/heads/master
Commit: a5c16b2917bee8d4a97362fd87cc85d45b4aab46
Parents: f763b3e
Author: Aled Sage <aled.sage@gmail.com>
Authored: Thu Jun 9 16:49:48 2016 +0100
Committer: Aled Sage <aled.sage@gmail.com>
Committed: Fri Jun 10 12:54:50 2016 +0100

----------------------------------------------------------------------
 .../core/network/AbstractOnNetworkEnricher.java | 407 +++++++++++++++++++
 .../core/network/OnPublicNetworkEnricher.java   | 357 +---------------
 .../core/network/OnSubnetNetworkEnricher.java   |  74 ++++
 .../network/OnPublicNetworkEnricherTest.java    |   4 +-
 .../network/OnSubnetNetworkEnricherTest.java    | 278 +++++++++++++
 5 files changed, 768 insertions(+), 352 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/a5c16b29/core/src/main/java/org/apache/brooklyn/core/network/AbstractOnNetworkEnricher.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/network/AbstractOnNetworkEnricher.java b/core/src/main/java/org/apache/brooklyn/core/network/AbstractOnNetworkEnricher.java
new file mode 100644
index 0000000..b7ee12b
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/network/AbstractOnNetworkEnricher.java
@@ -0,0 +1,407 @@
+/*
+ * 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.brooklyn.core.network;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Collection;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntityLocal;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.api.sensor.Sensor;
+import org.apache.brooklyn.api.sensor.SensorEvent;
+import org.apache.brooklyn.api.sensor.SensorEventListener;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.enricher.AbstractEnricher;
+import org.apache.brooklyn.core.entity.AbstractEntity;
+import org.apache.brooklyn.core.location.Machines;
+import org.apache.brooklyn.core.location.access.PortForwardManager;
+import org.apache.brooklyn.core.sensor.Sensors;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.net.Networking;
+import org.apache.brooklyn.util.text.StringPredicates;
+import org.apache.brooklyn.util.text.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Lists;
+import com.google.common.net.HostAndPort;
+import com.google.common.reflect.TypeToken;
+
+@Beta
+public abstract class AbstractOnNetworkEnricher extends AbstractEnricher {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AbstractOnNetworkEnricher.class);
+
+    @SuppressWarnings("serial")
+    public static final ConfigKey<AttributeSensor<?>> SENSOR = ConfigKeys.newConfigKey(
+            new TypeToken<AttributeSensor<?>>() {}, 
+            "sensor",
+            "The sensor whose mapped value is to be re-published (with suffix \"mapped.networkName\"); "
+                    + "either 'sensor' or 'sensors' should be specified");
+
+    @SuppressWarnings("serial")
+    public static ConfigKey<Collection<? extends AttributeSensor<?>>> SENSORS = ConfigKeys.newConfigKey(
+            new TypeToken<Collection<? extends AttributeSensor<?>>>() {}, 
+            "sensors",
+            "The multiple sensors whose mapped values are to be re-published (with suffix \"mapped.networkName\"); "
+                    + "if neither 'sensor' or 'sensors' is specified, defaults to 'mapAll'");
+
+    public static ConfigKey<String> MAP_MATCHING = ConfigKeys.newStringConfigKey(
+            "mapMatching",
+            "Whether to map all, based on a sensor naming convention (re-published with suffix \"mapped.networkName\"); "
+                    + "if neither 'sensor' or 'sensors' is specified, defaults to matchin case-insensitive suffix of "
+                    + "'port', 'uri', 'url' or 'endpoint' ",
+            "(?i).*(port|uri|url|endpoint)");
+
+    @SuppressWarnings("serial")
+    public static ConfigKey<Function<? super String, String>> SENSOR_NAME_CONVERTER = ConfigKeys.newConfigKey(
+            new TypeToken<Function<? super String, String>>() {},
+            "sensorNameConverter",
+            "The converter to use, to map from the original sensor name to the re-published sensor name");
+
+    public static class SensorNameConverter implements Function<String, String> {
+        private final String network;
+        
+        public SensorNameConverter(String network) {
+            this.network = network;
+        }
+        
+        @Override
+        public String apply(String input) {
+            if (input == null) throw new NullPointerException("Sensor name must not be null");
+            String lowerInput = input.toLowerCase();
+            if (lowerInput.endsWith("uri")) {
+                return input + ".mapped." + network;
+            } else if (lowerInput.endsWith("url")) {
+                return input + ".mapped." + network;
+            } else if (lowerInput.endsWith("endpoint")) {
+                return input + ".mapped." + network;
+            } else if (lowerInput.endsWith("port")) {
+                String prefix = input.substring(0, input.length() - "port".length());
+                if (prefix.endsWith(".")) prefix = prefix.substring(0, prefix.length() - 1);
+                return prefix + ".endpoint.mapped." + network;
+            } else {
+                return input + ".mapped." + network;
+            }
+        }
+    }
+
+    protected Collection<AttributeSensor<?>> sensors;
+    protected Optional<Predicate<Sensor<?>>> mapMatching;
+    protected Function<? super String, String> sensorNameConverter;
+    protected PortForwardManager.AssociationListener pfmListener;
+    
+    protected abstract Optional<HostAndPort> getMappedEndpoint(Entity source, MachineLocation machine, int port);
+    
+    @Override
+    public void setEntity(final EntityLocal entity) {
+        super.setEntity(entity);
+        
+        checkConfig();
+        sensors = resolveSensorsConfig();
+        if (sensors.isEmpty()) {
+            mapMatching = Optional.of(resolveMapMatchingConfig());
+        } else {
+            mapMatching = Optional.absent();
+        }
+        sensorNameConverter = getRequiredConfig(SENSOR_NAME_CONVERTER);
+        
+        subscriptions().subscribe(entity, AbstractEntity.LOCATION_ADDED, new SensorEventListener<Location>() {
+            @Override public void onEvent(SensorEvent<Location> event) {
+                LOG.debug("{} attempting transformations, triggered by location-added {}, to {}", new Object[] {AbstractOnNetworkEnricher.this, event.getValue(), entity});
+                tryTransformAll();
+            }});
+
+        for (AttributeSensor<?> sensor : sensors) {
+            subscriptions().subscribe(entity, sensor, new SensorEventListener<Object>() {
+                @Override public void onEvent(SensorEvent<Object> event) {
+                    LOG.debug("{} attempting transformations, triggered by sensor-event {}->{}, to {}", 
+                            new Object[] {AbstractOnNetworkEnricher.this, event.getSensor().getName(), event.getValue(), entity});
+                    tryTransform((AttributeSensor<?>)event.getSensor());
+                }});
+        }
+        if (mapMatching.isPresent()) {
+            Sensor<?> wildcardSensor = null;
+            subscriptions().subscribe(entity, wildcardSensor, new SensorEventListener<Object>() {
+                @Override public void onEvent(SensorEvent<Object> event) {
+                    if (mapMatching.get().apply(event.getSensor())) {
+                        LOG.debug("{} attempting transformations, triggered by sensor-event {}->{}, to {}", 
+                                new Object[] {AbstractOnNetworkEnricher.this, event.getSensor().getName(), event.getValue(), entity});
+                        tryTransform((AttributeSensor<?>)event.getSensor());
+                    }
+                }});
+        }
+
+        tryTransformAll();
+    }
+
+    protected void tryTransformAll() {
+        if (!isRunning()) {
+            return;
+        }
+        Maybe<MachineLocation> machine = getMachine();
+        if (machine.isAbsent()) {
+            return;
+        }
+        for (AttributeSensor<?> sensor : sensors) {
+            try {
+                tryTransform(machine.get(), sensor);
+            } catch (Exception e) {
+                // TODO Avoid repeated logging
+                Exceptions.propagateIfFatal(e);
+                LOG.warn("Problem transforming sensor "+sensor+" of "+entity, e);
+            }
+        }
+        if (mapMatching.isPresent()) {
+            for (Sensor<?> sensor : entity.getEntityType().getSensors()) {
+                if (sensor instanceof AttributeSensor && mapMatching.get().apply(sensor)) {
+                    try {
+                        tryTransform(machine.get(), (AttributeSensor<?>)sensor);
+                    } catch (Exception e) {
+                        // TODO Avoid repeated logging
+                        Exceptions.propagateIfFatal(e);
+                        LOG.warn("Problem transforming sensor "+sensor+" of "+entity, e);
+                    }
+                }
+            }
+        }
+    }
+
+    protected void tryTransform(AttributeSensor<?> sensor) {
+        if (!isRunning()) {
+            return;
+        }
+        Maybe<MachineLocation> machine = getMachine();
+        if (machine.isAbsent()) {
+            return;
+        }
+        tryTransform(machine.get(), sensor);
+    }
+    
+    protected void tryTransform(MachineLocation machine, AttributeSensor<?> sensor) {
+        Object sensorVal = entity.sensors().get(sensor);
+        if (sensorVal == null) {
+            return;
+        }
+        Maybe<String> newVal = transformVal(machine, sensor, sensorVal);
+        if (newVal.isAbsent()) {
+            return;
+        }
+        AttributeSensor<String> mappedSensor = Sensors.newStringSensor(sensorNameConverter.apply(sensor.getName()));
+        if (newVal.get().equals(entity.sensors().get(mappedSensor))) {
+            // ignore duplicate
+            return;
+        }
+        LOG.debug("{} publishing value {} (original sensor value {}) for mapped sensor {}, of entity {}", 
+                new Object[] {this, newVal.get(), sensorVal, mappedSensor, entity});
+        entity.sensors().set(mappedSensor, newVal.get());
+    }
+    
+    protected Maybe<String> transformVal(MachineLocation machine, AttributeSensor<?> sensor, Object sensorVal) {
+        if (sensorVal == null) {
+            return Maybe.absent();
+        } else if (isPort(sensorVal)) {
+            int port = toInteger(sensorVal);
+            return transformPort(entity, machine, port);
+        } else if (isUri(sensorVal)) {
+            return transformUri(entity, machine, sensorVal.toString());
+        } else if (isHostAndPort(sensorVal)) {
+            return transformHostAndPort(entity, machine, sensorVal.toString());
+        } else {
+            // no-op; unrecognised type
+            return Maybe.absent();
+        }
+    }
+
+    protected boolean isUri(Object sensorVal) {
+        if (sensorVal instanceof URI || sensorVal instanceof URL) {
+            return true;
+        }
+        try {
+            URI uri = new URI(sensorVal.toString());
+            return uri.getScheme() != null;
+        } catch (URISyntaxException e) {
+            return false;
+        }
+    }
+
+    protected boolean isPort(Object sensorVal) {
+        if (sensorVal instanceof Integer || sensorVal instanceof Long) {
+            return Networking.isPortValid(((Number)sensorVal).intValue());
+        } else if (sensorVal instanceof CharSequence) {
+            return sensorVal.toString().trim().matches("[0-9]+");
+        } else {
+            return false;
+        }
+    }
+
+    protected int toInteger(Object sensorVal) {
+        if (sensorVal instanceof Number) {
+            return ((Number)sensorVal).intValue();
+        } else if (sensorVal instanceof CharSequence) {
+            return Integer.parseInt(sensorVal.toString().trim());
+        } else {
+            throw new IllegalArgumentException("Expected number but got "+sensorVal+" of type "+(sensorVal != null ? sensorVal.getClass() : null));
+        }
+    }
+
+    protected boolean isHostAndPort(Object sensorVal) {
+        if (sensorVal instanceof HostAndPort) {
+            return true;
+        } else if (sensorVal instanceof String) {
+            try {
+                HostAndPort hostAndPort = HostAndPort.fromString((String)sensorVal);
+                return hostAndPort.hasPort();
+            } catch (IllegalArgumentException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    protected Maybe<String> transformUri(Entity source, MachineLocation machine, String sensorVal) {
+        URI uri = URI.create(sensorVal);
+        int port = uri.getPort();
+        if (port == -1 && "http".equalsIgnoreCase(uri.getScheme())) port = 80;
+        if (port == -1 && "https".equalsIgnoreCase(uri.getScheme())) port = 443;
+
+        if (port != -1) {
+            Optional<HostAndPort> mappedEndpoint = getMappedEndpoint(source, machine, port);
+            if (!mappedEndpoint.isPresent()) {
+                LOG.trace("network-facing enricher not transforming {} URI {}, because no port-mapping for {}", new Object[] {source, sensorVal, machine});
+                return Maybe.absent();
+            }
+            if (!mappedEndpoint.get().hasPort()) {
+                LOG.debug("network-facing enricher not transforming {} URI {}, because no port in target {} for {}", new Object[] {source, sensorVal, mappedEndpoint, machine});
+                return Maybe.absent();
+            }
+            URI result;
+            try {
+                result = new URI(uri.getScheme(), uri.getUserInfo(), mappedEndpoint.get().getHostText(), mappedEndpoint.get().getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
+            } catch (URISyntaxException e) {
+                LOG.debug("Error transforming URI "+uri+", using target "+mappedEndpoint+"; rethrowing");
+                throw Exceptions.propagateAnnotated("Error transforming URI "+uri+", using target "+mappedEndpoint, e);
+            }
+            return Maybe.of(result.toString());
+        } else {
+            LOG.debug("sensor mapper not transforming URI "+uri+" because no port defined");
+            return Maybe.absent();
+        }
+    }
+
+    protected Maybe<String> transformHostAndPort(Entity source, MachineLocation machine, String sensorVal) {
+        HostAndPort hostAndPort = HostAndPort.fromString(sensorVal);
+        if (hostAndPort.hasPort()) {
+            int port = hostAndPort.getPort();
+            Optional<HostAndPort> mappedEndpoint = getMappedEndpoint(source, machine, port);
+            if (!mappedEndpoint.isPresent()) {
+                LOG.debug("network-facing enricher not transforming {} host-and-port {}, because no port-mapping for {}", new Object[] {source, sensorVal, machine});
+                return Maybe.absent();
+            }
+            if (!mappedEndpoint.get().hasPort()) {
+                LOG.debug("network-facing enricher not transforming {} host-and-port {}, because no port in target {} for {}", new Object[] {source, sensorVal, mappedEndpoint, machine});
+                return Maybe.absent();
+            }
+            return Maybe.of(mappedEndpoint.get().toString());
+        } else {
+            LOG.debug("network-facing enricher not transforming {} host-and-port {} because defines no port", source, hostAndPort);
+            return Maybe.absent();
+        }
+    }
+
+    protected Maybe<String> transformPort(Entity source, MachineLocation machine, int sensorVal) {
+        if (Networking.isPortValid(sensorVal)) {
+            Optional<HostAndPort> mappedEndpoint = getMappedEndpoint(source, machine, sensorVal);
+            if (!mappedEndpoint.isPresent()) {
+                LOG.debug("network-facing enricher not transforming {} port {}, because no port-mapping for {}", new Object[] {source, sensorVal, machine});
+                return Maybe.absent();
+            }
+            if (!mappedEndpoint.get().hasPort()) {
+                LOG.debug("network-facing enricher not transforming {} port {}, because no port in target {} for {}", new Object[] {source, sensorVal, mappedEndpoint, machine});
+                return Maybe.absent();
+            }
+            return Maybe.of(mappedEndpoint.get().toString());
+        } else {
+            LOG.debug("network-facing enricher not transforming {} port {} because not a valid port", source, sensorVal);
+            return Maybe.absent();
+        }
+    }
+
+    protected Maybe<MachineLocation> getMachine() {
+        return Machines.findUniqueMachineLocation(entity.getLocations());
+    }
+    
+    protected void checkConfig() {
+        AttributeSensor<?> sensor = getConfig(SENSOR);
+        Collection<? extends AttributeSensor<?>> sensors = getConfig(SENSORS);
+        Maybe<Object> rawMapMatching = config().getRaw(MAP_MATCHING);
+        String mapMatching = config().get(MAP_MATCHING);
+        
+        if (sensor != null && sensors != null && !sensors.isEmpty()) {
+            throw new IllegalStateException(this+" must not have both 'sensor' and 'sensors' config");
+        } else if (sensor == null && (sensors == null || sensors.isEmpty())) {
+            if (Strings.isBlank(mapMatching)) {
+                throw new IllegalStateException(this+" requires one of 'sensor' or 'sensors' config (when 'mapMatching' is explicitly blank)");
+            }
+        } else if (rawMapMatching.isPresent()) {
+            throw new IllegalStateException(this+" must not have explicit 'mapMatching', and either of 'sensor' or 'sensors' config");
+        }
+    }
+    
+    protected Collection<AttributeSensor<?>> resolveSensorsConfig() {
+        AttributeSensor<?> sensor = getConfig(SENSOR);
+        Collection<? extends AttributeSensor<?>> sensors = getConfig(SENSORS);
+
+        Collection<AttributeSensor<?>> result = Lists.newArrayList();
+        if (sensor != null) {
+            AttributeSensor<?> typedSensor = (AttributeSensor<?>) entity.getEntityType().getSensor(sensor.getName());
+            result.add(typedSensor != null ? typedSensor : sensor);
+        }
+        if (sensors != null) {
+            for (Object s : sensors) {
+                AttributeSensor<?> coercedSensor = TypeCoercions.coerce(s, AttributeSensor.class);
+                AttributeSensor<?> typedSensor = (AttributeSensor<?>) entity.getEntityType().getSensor(coercedSensor.getName());
+                result.add(typedSensor != null ? typedSensor : sensor);
+            }
+        }
+        return result;
+    }
+    
+    protected Predicate<Sensor<?>> resolveMapMatchingConfig() {
+        String regex = getConfig(MAP_MATCHING);
+        final Predicate<CharSequence> namePredicate = StringPredicates.matchesRegex(regex);
+        return new Predicate<Sensor<?>>() {
+            @Override public boolean apply(Sensor<?> input) {
+                return input != null && namePredicate.apply(input.getName());
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/a5c16b29/core/src/main/java/org/apache/brooklyn/core/network/OnPublicNetworkEnricher.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/network/OnPublicNetworkEnricher.java b/core/src/main/java/org/apache/brooklyn/core/network/OnPublicNetworkEnricher.java
index ca9e140..d37aa6d 100644
--- a/core/src/main/java/org/apache/brooklyn/core/network/OnPublicNetworkEnricher.java
+++ b/core/src/main/java/org/apache/brooklyn/core/network/OnPublicNetworkEnricher.java
@@ -18,44 +18,22 @@
  */
 package org.apache.brooklyn.core.network;
 
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.util.Collection;
-
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.EntityLocal;
-import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.api.location.MachineLocation;
-import org.apache.brooklyn.api.sensor.AttributeSensor;
-import org.apache.brooklyn.api.sensor.Sensor;
-import org.apache.brooklyn.api.sensor.SensorEvent;
-import org.apache.brooklyn.api.sensor.SensorEventListener;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
-import org.apache.brooklyn.core.enricher.AbstractEnricher;
-import org.apache.brooklyn.core.entity.AbstractEntity;
-import org.apache.brooklyn.core.location.Machines;
 import org.apache.brooklyn.core.location.access.PortForwardManager;
 import org.apache.brooklyn.core.location.access.PortForwardManagerLocationResolver;
-import org.apache.brooklyn.core.sensor.Sensors;
-import org.apache.brooklyn.util.core.flags.TypeCoercions;
-import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
-import org.apache.brooklyn.util.net.Networking;
-import org.apache.brooklyn.util.text.StringPredicates;
-import org.apache.brooklyn.util.text.Strings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.Function;
 import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
-import com.google.common.collect.Lists;
 import com.google.common.net.HostAndPort;
-import com.google.common.reflect.TypeToken;
 
 /**
  * Can be added to an entity so that it advertises its mapped ports (according to the port-mappings
@@ -86,10 +64,8 @@ import com.google.common.reflect.TypeToken;
  * </pre>
  */
 @Beta
-public class OnPublicNetworkEnricher extends AbstractEnricher {
+public class OnPublicNetworkEnricher extends AbstractOnNetworkEnricher {
 
-    // TODO Is this the best package for the enricher?
-    //
     // TODO Need more logging, particularly for when the value has *not* been transformed.
     //
     // TODO What if the sensor has an unrelated hostname - we will currently still transform this!
@@ -105,32 +81,8 @@ public class OnPublicNetworkEnricher extends AbstractEnricher {
 
     private static final Logger LOG = LoggerFactory.getLogger(OnPublicNetworkEnricher.class);
 
-    @SuppressWarnings("serial")
-    public static final ConfigKey<AttributeSensor<?>> SENSOR = ConfigKeys.newConfigKey(
-            new TypeToken<AttributeSensor<?>>() {}, 
-            "sensor",
-            "The sensor whose mapped value is to be re-published (with suffix \"mapped.public\"); "
-                    + "either 'sensor' or 'sensors' should be specified");
-
-    @SuppressWarnings("serial")
-    public static ConfigKey<Collection<? extends AttributeSensor<?>>> SENSORS = ConfigKeys.newConfigKey(
-            new TypeToken<Collection<? extends AttributeSensor<?>>>() {}, 
-            "sensors",
-            "The multiple sensors whose mapped values are to be re-published (with suffix \"mapped.public\"); "
-                    + "if neither 'sensor' or 'sensors' is specified, defaults to 'mapAll'");
-
-    public static ConfigKey<String> MAP_MATCHING = ConfigKeys.newStringConfigKey(
-            "mapMatching",
-            "Whether to map all, based on a sensor naming convention (re-published with suffix \"mapped.public\"); "
-                    + "if neither 'sensor' or 'sensors' is specified, defaults to matchin case-insensitive suffix of "
-                    + "'port', 'uri', 'url' or 'endpoint' ",
-            "(?i).*(port|uri|url|endpoint)");
-
-    @SuppressWarnings("serial")
-    public static ConfigKey<Function<? super String, String>> SENSOR_NAME_CONVERTER = ConfigKeys.newConfigKey(
-            new TypeToken<Function<? super String, String>>() {},
-            "sensorNameConverter",
-            "The converter to use, to map from the original sensor name to the re-published sensor name",
+    public static ConfigKey<Function<? super String, String>> SENSOR_NAME_CONVERTER = ConfigKeys.newConfigKeyWithDefault(
+            AbstractOnNetworkEnricher.SENSOR_NAME_CONVERTER,
             new SensorNameConverter("public"));
 
     public static final ConfigKey<PortForwardManager> PORT_FORWARD_MANAGER = ConfigKeys.newConfigKey(
@@ -138,51 +90,12 @@ public class OnPublicNetworkEnricher extends AbstractEnricher {
             "portForwardManager",
             "The PortForwardManager storing the port-mappings; if null, the global instance will be used");
     
-    public static class SensorNameConverter implements Function<String, String> {
-        private final String network;
-        
-        public SensorNameConverter(String network) {
-            this.network = network;
-        }
-        
-        @Override
-        public String apply(String input) {
-            if (input == null) throw new NullPointerException("Sensor name must not be null");
-            String lowerInput = input.toLowerCase();
-            if (lowerInput.endsWith("uri")) {
-                return input + ".mapped." + network;
-            } else if (lowerInput.endsWith("url")) {
-                return input + ".mapped." + network;
-            } else if (lowerInput.endsWith("endpoint")) {
-                return input + ".mapped." + network;
-            } else if (lowerInput.endsWith("port")) {
-                String prefix = input.substring(0, input.length() - "port".length());
-                if (prefix.endsWith(".")) prefix = prefix.substring(0, prefix.length() - 1);
-                return prefix + ".endpoint.mapped." + network;
-            } else {
-                return input + ".mapped." + network;
-            }
-        }
-    }
-
-    protected Collection<AttributeSensor<?>> sensors;
-    protected Optional<Predicate<Sensor<?>>> mapMatching;
-    protected Function<? super String, String> sensorNameConverter;
     protected PortForwardManager.AssociationListener pfmListener;
     
     @Override
     public void setEntity(final EntityLocal entity) {
         super.setEntity(entity);
         
-        checkConfig();
-        sensors = resolveSensorsConfig();
-        if (sensors.isEmpty()) {
-            mapMatching = Optional.of(resolveMapMatchingConfig());
-        } else {
-            mapMatching = Optional.absent();
-        }
-        sensorNameConverter = getRequiredConfig(SENSOR_NAME_CONVERTER);
-        
         /*
          * To find the transformed sensor value we need several things to be set. Therefore 
          * subscribe to all of them, and re-compute whenever any of the change. These are:
@@ -209,34 +122,6 @@ public class OnPublicNetworkEnricher extends AbstractEnricher {
             }
         };
         getPortForwardManager().addAssociationListener(pfmListener, Predicates.alwaysTrue());
-        
-        subscriptions().subscribe(entity, AbstractEntity.LOCATION_ADDED, new SensorEventListener<Location>() {
-            @Override public void onEvent(SensorEvent<Location> event) {
-                LOG.debug("{} attempting transformations, triggered by location-added {}, to {}", new Object[] {OnPublicNetworkEnricher.this, event.getValue(), entity});
-                tryTransformAll();
-            }});
-
-        for (AttributeSensor<?> sensor : sensors) {
-            subscriptions().subscribe(entity, sensor, new SensorEventListener<Object>() {
-                @Override public void onEvent(SensorEvent<Object> event) {
-                    LOG.debug("{} attempting transformations, triggered by sensor-event {}->{}, to {}", 
-                            new Object[] {OnPublicNetworkEnricher.this, event.getSensor().getName(), event.getValue(), entity});
-                    tryTransform((AttributeSensor<?>)event.getSensor());
-                }});
-        }
-        if (mapMatching.isPresent()) {
-            Sensor<?> wildcardSensor = null;
-            subscriptions().subscribe(entity, wildcardSensor, new SensorEventListener<Object>() {
-                @Override public void onEvent(SensorEvent<Object> event) {
-                    if (mapMatching.get().apply(event.getSensor())) {
-                        LOG.debug("{} attempting transformations, triggered by sensor-event {}->{}, to {}", 
-                                new Object[] {OnPublicNetworkEnricher.this, event.getSensor().getName(), event.getValue(), entity});
-                        tryTransform((AttributeSensor<?>)event.getSensor());
-                    }
-                }});
-        }
-
-        tryTransformAll();
     }
 
     @Override
@@ -250,192 +135,10 @@ public class OnPublicNetworkEnricher extends AbstractEnricher {
         }
     }
 
-    protected void tryTransformAll() {
-        if (!isRunning()) {
-            return;
-        }
-        Maybe<MachineLocation> machine = getMachine();
-        if (machine.isAbsent()) {
-            return;
-        }
-        for (AttributeSensor<?> sensor : sensors) {
-            try {
-                tryTransform(machine.get(), sensor);
-            } catch (Exception e) {
-                // TODO Avoid repeated logging
-                Exceptions.propagateIfFatal(e);
-                LOG.warn("Problem transforming sensor "+sensor+" of "+entity, e);
-            }
-        }
-        if (mapMatching.isPresent()) {
-            for (Sensor<?> sensor : entity.getEntityType().getSensors()) {
-                if (sensor instanceof AttributeSensor && mapMatching.get().apply(sensor)) {
-                    try {
-                        tryTransform(machine.get(), (AttributeSensor<?>)sensor);
-                    } catch (Exception e) {
-                        // TODO Avoid repeated logging
-                        Exceptions.propagateIfFatal(e);
-                        LOG.warn("Problem transforming sensor "+sensor+" of "+entity, e);
-                    }
-                }
-            }
-        }
-    }
-
-    protected void tryTransform(AttributeSensor<?> sensor) {
-        if (!isRunning()) {
-            return;
-        }
-        Maybe<MachineLocation> machine = getMachine();
-        if (machine.isAbsent()) {
-            return;
-        }
-        tryTransform(machine.get(), sensor);
-    }
-    
-    protected void tryTransform(MachineLocation machine, AttributeSensor<?> sensor) {
-        Object sensorVal = entity.sensors().get(sensor);
-        if (sensorVal == null) {
-            return;
-        }
-        Maybe<String> newVal = transformVal(machine, sensor, sensorVal);
-        if (newVal.isAbsent()) {
-            return;
-        }
-        AttributeSensor<String> mappedSensor = Sensors.newStringSensor(sensorNameConverter.apply(sensor.getName()));
-        if (newVal.get().equals(entity.sensors().get(mappedSensor))) {
-            // ignore duplicate
-            return;
-        }
-        LOG.debug("{} publishing value {} for transformed sensor {}, of entity {}", 
-                new Object[] {this, newVal.get(), sensor, entity});
-        entity.sensors().set(mappedSensor, newVal.get());
-    }
-    
-    protected Maybe<String> transformVal(MachineLocation machine, AttributeSensor<?> sensor, Object sensorVal) {
-        if (sensorVal == null) {
-            return Maybe.absent();
-        } else if (isPort(sensorVal)) {
-            int port = toInteger(sensorVal);
-            return transformPort(entity, machine, port);
-        } else if (isUri(sensorVal)) {
-            return transformUri(entity, machine, sensorVal.toString());
-        } else if (isHostAndPort(sensorVal)) {
-            return transformHostAndPort(entity, machine, sensorVal.toString());
-        } else {
-            // no-op; unrecognised type
-            return Maybe.absent();
-        }
-    }
-
-    protected boolean isUri(Object sensorVal) {
-        if (sensorVal instanceof URI || sensorVal instanceof URL) {
-            return true;
-        }
-        try {
-            URI uri = new URI(sensorVal.toString());
-            return uri.getScheme() != null;
-        } catch (URISyntaxException e) {
-            return false;
-        }
-    }
-
-    protected boolean isPort(Object sensorVal) {
-        if (sensorVal instanceof Integer || sensorVal instanceof Long) {
-            return Networking.isPortValid(((Number)sensorVal).intValue());
-        } else if (sensorVal instanceof CharSequence) {
-            return sensorVal.toString().trim().matches("[0-9]+");
-        } else {
-            return false;
-        }
-    }
-
-    protected int toInteger(Object sensorVal) {
-        if (sensorVal instanceof Number) {
-            return ((Number)sensorVal).intValue();
-        } else if (sensorVal instanceof CharSequence) {
-            return Integer.parseInt(sensorVal.toString().trim());
-        } else {
-            throw new IllegalArgumentException("Expected number but got "+sensorVal+" of type "+(sensorVal != null ? sensorVal.getClass() : null));
-        }
-    }
-
-    protected boolean isHostAndPort(Object sensorVal) {
-        if (sensorVal instanceof HostAndPort) {
-            return true;
-        } else if (sensorVal instanceof String) {
-            try {
-                HostAndPort hostAndPort = HostAndPort.fromString((String)sensorVal);
-                return hostAndPort.hasPort();
-            } catch (IllegalArgumentException e) {
-                return false;
-            }
-        }
-        return false;
-    }
-
-    protected Maybe<String> transformUri(Entity source, MachineLocation machine, String sensorVal) {
-        URI uri = URI.create(sensorVal);
-        int port = uri.getPort();
-        if (port == -1 && "http".equalsIgnoreCase(uri.getScheme())) port = 80;
-        if (port == -1 && "https".equalsIgnoreCase(uri.getScheme())) port = 443;
-
-        if (port != -1) {
-            HostAndPort publicTarget = getPortForwardManager().lookup(machine, port);
-            if (publicTarget == null) {
-                LOG.trace("network-facing enricher not transforming {} URI {}, because no port-mapping for {}", new Object[] {source, sensorVal, machine});
-                return Maybe.absent();
-            }
-            if (!publicTarget.hasPort()) {
-                LOG.debug("network-facing enricher not transforming {} URI {}, because no port in public-target {} for {}", new Object[] {source, sensorVal, publicTarget, machine});
-                return Maybe.absent();
-            }
-            URI result;
-            try {
-                result = new URI(uri.getScheme(), uri.getUserInfo(), publicTarget.getHostText(), publicTarget.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
-            } catch (URISyntaxException e) {
-                LOG.debug("Error transforming URI "+uri+", using target "+publicTarget+"; rethrowing");
-                throw Exceptions.propagateAnnotated("Error transforming URI "+uri+", using target "+publicTarget, e);
-            }
-            return Maybe.of(result.toString());
-        } else {
-            LOG.debug("sensor mapper not transforming URI "+uri+" because no port defined");
-            return Maybe.absent();
-        }
-    }
-
-    protected Maybe<String> transformHostAndPort(Entity source, MachineLocation machine, String sensorVal) {
-        HostAndPort hostAndPort = HostAndPort.fromString(sensorVal);
-        if (hostAndPort.hasPort()) {
-            int port = hostAndPort.getPort();
-            HostAndPort publicTarget = getPortForwardManager().lookup(machine, port);
-            if (publicTarget == null) {
-                LOG.debug("network-facing enricher not transforming {} host-and-port {}, because no port-mapping for {}", new Object[] {source, sensorVal, machine});
-                return Maybe.absent();
-            }
-            return Maybe.of(publicTarget.toString());
-        } else {
-            LOG.debug("network-facing enricher not transforming {} host-and-port {} because defines no port", source, hostAndPort);
-            return Maybe.absent();
-        }
-    }
-
-    protected Maybe<String> transformPort(Entity source, MachineLocation machine, int sensorVal) {
-        if (Networking.isPortValid(sensorVal)) {
-            HostAndPort publicTarget = getPortForwardManager().lookup(machine, sensorVal);
-            if (publicTarget == null) {
-                LOG.debug("network-facing enricher not transforming {} host-and-port {}, because no port-mapping for {}", new Object[] {source, sensorVal, machine});
-                return Maybe.absent();
-            }
-            return Maybe.of(publicTarget.toString());
-        } else {
-            LOG.debug("network-facing enricher not transforming {} port {} because not a valid port", source, sensorVal);
-            return Maybe.absent();
-        }
-    }
-
-    protected Maybe<MachineLocation> getMachine() {
-        return Machines.findUniqueMachineLocation(entity.getLocations());
+    @Override
+    protected Optional<HostAndPort> getMappedEndpoint(Entity source, MachineLocation machine, int port) {
+        HostAndPort publicTarget = getPortForwardManager().lookup(machine, port);
+        return Optional.fromNullable(publicTarget);
     }
     
     protected PortForwardManager getPortForwardManager() {
@@ -445,50 +148,4 @@ public class OnPublicNetworkEnricher extends AbstractEnricher {
         }
         return portForwardManager;
     }
-
-    protected void checkConfig() {
-        AttributeSensor<?> sensor = getConfig(SENSOR);
-        Collection<? extends AttributeSensor<?>> sensors = getConfig(SENSORS);
-        Maybe<Object> rawMapMatching = config().getRaw(MAP_MATCHING);
-        String mapMatching = config().get(MAP_MATCHING);
-        
-        if (sensor != null && sensors != null && !sensors.isEmpty()) {
-            throw new IllegalStateException(this+" must not have both 'sensor' and 'sensors' config");
-        } else if (sensor == null && (sensors == null || sensors.isEmpty())) {
-            if (Strings.isBlank(mapMatching)) {
-                throw new IllegalStateException(this+" requires one of 'sensor' or 'sensors' config (when 'mapMatching' is explicitly blank)");
-            }
-        } else if (rawMapMatching.isPresent()) {
-            throw new IllegalStateException(this+" must not have explicit 'mapMatching', and either of 'sensor' or 'sensors' config");
-        }
-    }
-    
-    protected Collection<AttributeSensor<?>> resolveSensorsConfig() {
-        AttributeSensor<?> sensor = getConfig(SENSOR);
-        Collection<? extends AttributeSensor<?>> sensors = getConfig(SENSORS);
-
-        Collection<AttributeSensor<?>> result = Lists.newArrayList();
-        if (sensor != null) {
-            AttributeSensor<?> typedSensor = (AttributeSensor<?>) entity.getEntityType().getSensor(sensor.getName());
-            result.add(typedSensor != null ? typedSensor : sensor);
-        }
-        if (sensors != null) {
-            for (Object s : sensors) {
-                AttributeSensor<?> coercedSensor = TypeCoercions.coerce(s, AttributeSensor.class);
-                AttributeSensor<?> typedSensor = (AttributeSensor<?>) entity.getEntityType().getSensor(coercedSensor.getName());
-                result.add(typedSensor != null ? typedSensor : sensor);
-            }
-        }
-        return result;
-    }
-    
-    protected Predicate<Sensor<?>> resolveMapMatchingConfig() {
-        String regex = getConfig(MAP_MATCHING);
-        final Predicate<CharSequence> namePredicate = StringPredicates.matchesRegex(regex);
-        return new Predicate<Sensor<?>>() {
-            @Override public boolean apply(Sensor<?> input) {
-                return input != null && namePredicate.apply(input.getName());
-            }
-        };
-    }
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/a5c16b29/core/src/main/java/org/apache/brooklyn/core/network/OnSubnetNetworkEnricher.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/network/OnSubnetNetworkEnricher.java b/core/src/main/java/org/apache/brooklyn/core/network/OnSubnetNetworkEnricher.java
new file mode 100644
index 0000000..064eb57
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/network/OnSubnetNetworkEnricher.java
@@ -0,0 +1,74 @@
+/*
+ * 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.brooklyn.core.network;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.location.access.PortForwardManager;
+import org.apache.brooklyn.util.text.Strings;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.net.HostAndPort;
+
+/**
+ * TODO This is a temporary measure while we discuss and implement the proposal 
+ * "Working with Multiple Networks": 
+ *     https://docs.google.com/document/d/1IrWLWunWSl_ScwY3MRICped8eJMjQEH1FbWZJcoK0Iw/edit#heading=h.gwaayi613qqk
+ * 
+ * Can be added to an entity so that it advertises its subnet ports (by concatenating the subnet.address
+ * and the port number).
+ * 
+ * For example, to configure MySQL to publish "datastore.url.mapped.subnet":
+ * <pre>
+ * {@code
+ * services:
+ * - type: org.apache.brooklyn.entity.database.mysql.MySqlNode
+ *   brooklyn.enrichers:
+ *   - type: org.apache.brooklyn.core.network.OnSubnetNetworkEnricher
+ *     brooklyn.config:
+ *       sensor: datastore.url
+ * }
+ * </pre>
+ */
+@Beta
+public class OnSubnetNetworkEnricher extends AbstractOnNetworkEnricher {
+
+    // TODO This is a temporary measure while we discuss and implement 
+    // the proposal "Working with Multiple Networks": 
+    // https://docs.google.com/document/d/1IrWLWunWSl_ScwY3MRICped8eJMjQEH1FbWZJcoK0Iw/edit#heading=h.gwaayi613qqk
+
+    public static ConfigKey<Function<? super String, String>> SENSOR_NAME_CONVERTER = ConfigKeys.newConfigKeyWithDefault(
+            AbstractOnNetworkEnricher.SENSOR_NAME_CONVERTER,
+            new SensorNameConverter("subnet"));
+
+    @Override
+    protected Optional<HostAndPort> getMappedEndpoint(Entity source, MachineLocation machine, int port) {
+        String address = source.sensors().get(Attributes.SUBNET_ADDRESS);
+        if (Strings.isNonBlank(address)) {
+            return Optional.of(HostAndPort.fromParts(address, port));
+        } else {
+            return Optional.absent();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/a5c16b29/core/src/test/java/org/apache/brooklyn/core/network/OnPublicNetworkEnricherTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/network/OnPublicNetworkEnricherTest.java b/core/src/test/java/org/apache/brooklyn/core/network/OnPublicNetworkEnricherTest.java
index 0b1bb80..fd19c11 100644
--- a/core/src/test/java/org/apache/brooklyn/core/network/OnPublicNetworkEnricherTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/network/OnPublicNetworkEnricherTest.java
@@ -70,7 +70,7 @@ public class OnPublicNetworkEnricherTest extends BrooklynAppUnitTestSupport {
     }
     
     @DataProvider(name = "variants")
-    public static Object[][] provideVariants() {
+    public Object[][] provideVariants() {
         AttributeSensor<HostAndPort> hostAndPortSensor = Sensors.newSensor(HostAndPort.class, "test.endpoint");
         List<Object[]> result = Lists.newArrayList();
         for (Timing setSensor : Timing.values()) {
@@ -95,7 +95,7 @@ public class OnPublicNetworkEnricherTest extends BrooklynAppUnitTestSupport {
     }
     
     @DataProvider(name = "invalidVariants")
-    public static Object[][] provideInvalidVariants() {
+    public Object[][] provideInvalidVariants() {
         AttributeSensor<HostAndPort> hostAndPortSensor = Sensors.newSensor(HostAndPort.class, "test.hostAndPort");
         List<Object[]> result = Lists.newArrayList();
         result.add(new Object[] {Attributes.MAIN_URI, (URI)null});

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/a5c16b29/core/src/test/java/org/apache/brooklyn/core/network/OnSubnetNetworkEnricherTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/network/OnSubnetNetworkEnricherTest.java b/core/src/test/java/org/apache/brooklyn/core/network/OnSubnetNetworkEnricherTest.java
new file mode 100644
index 0000000..3345dc8
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/network/OnSubnetNetworkEnricherTest.java
@@ -0,0 +1,278 @@
+/*
+ * 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.brooklyn.core.network;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.api.sensor.EnricherSpec;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.entity.EntityAsserts;
+import org.apache.brooklyn.core.sensor.Sensors;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.core.test.entity.TestEntity;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.time.Duration;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.net.HostAndPort;
+
+public class OnSubnetNetworkEnricherTest extends BrooklynAppUnitTestSupport {
+
+    private static final Duration VERY_SHORT_WAIT = Duration.millis(100);
+
+    private final String privateIp = "10.179.184.237"; // An example private ip (only accessible within the subnet)
+    private final String publicIp = "54.158.173.158"; // An example public ip
+
+    private TestEntity entity;
+    private SshMachineLocation machine;
+
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        entity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .configure("address", "1.2.3.4"));
+    }
+    
+    @DataProvider(name = "variants")
+    public Object[][] provideVariants() {
+        AttributeSensor<HostAndPort> hostAndPortSensor = Sensors.newSensor(HostAndPort.class, "test.endpoint");
+        List<Object[]> result = Lists.newArrayList();
+        for (Timing setSensor : Timing.values()) {
+            for (Timing addLocation : Timing.values()) {
+                result.add(new Object[] {setSensor, addLocation, Attributes.MAIN_URI, 
+                        URI.create("http://"+publicIp+":1234/my/path"), "main.uri.mapped.subnet", "http://"+privateIp+":1234/my/path"});
+                result.add(new Object[] {setSensor, addLocation, TestEntity.NAME, 
+                        "http://"+publicIp+":1234/my/path", "test.name.mapped.subnet", "http://"+privateIp+":1234/my/path"});
+                result.add(new Object[] {setSensor, addLocation, Attributes.HTTP_PORT, 
+                        1234, "http.endpoint.mapped.subnet", privateIp+":1234"});
+                result.add(new Object[] {setSensor, addLocation, TestEntity.NAME, 
+                        "1234", "test.name.mapped.subnet", privateIp+":1234"});
+                result.add(new Object[] {setSensor, addLocation, TestEntity.NAME, 
+                        publicIp+":1234", "test.name.mapped.subnet", privateIp+":1234"});
+                result.add(new Object[] {setSensor, addLocation, hostAndPortSensor, 
+                        HostAndPort.fromString(publicIp+":1234"), "test.endpoint.mapped.subnet", privateIp+":1234"});
+            }
+        }
+        return result.toArray(new Object[result.size()][]);
+    }
+    
+    @DataProvider(name = "invalidVariants")
+    public Object[][] provideInvalidVariants() {
+        AttributeSensor<HostAndPort> hostAndPortSensor = Sensors.newSensor(HostAndPort.class, "test.hostAndPort");
+        List<Object[]> result = Lists.newArrayList();
+        result.add(new Object[] {Attributes.MAIN_URI, (URI)null});
+        result.add(new Object[] {TestEntity.NAME, publicIp+":1234/my/path"}); // must have scheme
+        result.add(new Object[] {Attributes.HTTP_PORT, null});
+        result.add(new Object[] {Attributes.HTTP_PORT, 1234567});
+        result.add(new Object[] {TestEntity.NAME, null});
+        result.add(new Object[] {TestEntity.NAME, "1234567"});
+        result.add(new Object[] {TestEntity.NAME, "thisHasNoPort"});
+        result.add(new Object[] {TestEntity.NAME, "portIsTooBig:1234567"});
+        result.add(new Object[] {hostAndPortSensor, (HostAndPort)null});
+        return result.toArray(new Object[result.size()][]);
+    }
+
+    enum Timing {
+        BEFORE,
+        AFTER;
+    }
+
+    /**
+     * The sensorVal must include port 1234, so that it will be converted to "+publicIp+":1234
+     */
+    @Test(dataProvider = "variants")
+    public <T> void testSensorTransformed(Timing setUri, Timing addLocation, 
+            AttributeSensor<T> sensor, T sensorVal, String targetSensorName, String expectedVal) throws Exception {
+        entity.sensors().set(Attributes.SUBNET_ADDRESS, privateIp);
+        if (setUri == Timing.BEFORE) {
+            entity.sensors().set(sensor, sensorVal);
+        }
+        if (addLocation == Timing.BEFORE) {
+            entity.addLocations(ImmutableList.of(machine));
+        }
+        
+        entity.enrichers().add(EnricherSpec.create(OnSubnetNetworkEnricher.class)
+                .configure(OnSubnetNetworkEnricher.SENSOR, sensor));
+
+        if (setUri == Timing.AFTER) {
+            entity.sensors().set(sensor, sensorVal);
+        }
+        if (addLocation == Timing.AFTER) {
+            entity.addLocations(ImmutableList.of(machine));
+        }
+        
+        EntityAsserts.assertAttributeEqualsEventually(entity, Sensors.newStringSensor(targetSensorName), expectedVal);
+        EntityAsserts.assertAttributeEquals(entity, sensor, sensorVal);
+    }
+    
+    
+    @Test(dataProvider = "invalidVariants")
+    public <T> void testIgnoresWhenInvalidAttribute(AttributeSensor<T> sensor, T sensorVal) throws Exception {
+        entity.sensors().set(Attributes.SUBNET_ADDRESS, "127.0.0.1");
+        entity.sensors().set(sensor, sensorVal);
+        entity.addLocations(ImmutableList.of(machine));
+        
+        entity.enrichers().add(EnricherSpec.create(OnSubnetNetworkEnricher.class)
+                .configure(OnSubnetNetworkEnricher.SENSOR, sensor));
+
+        EntityAsserts.assertAttributeEqualsContinually(ImmutableMap.of("timeout", VERY_SHORT_WAIT), entity, Sensors.newStringSensor(sensor.getName()+".mapped.subnet"), null);
+    }
+
+    @Test
+    public void testIgnoresWhenNoSubnetAddress() throws Exception {
+        entity.sensors().set(Attributes.HTTP_PORT, 1234);
+        
+        entity.enrichers().add(EnricherSpec.create(OnPublicNetworkEnricher.class)
+                .configure(OnPublicNetworkEnricher.SENSOR, Attributes.HTTP_PORT));
+
+        EntityAsserts.assertAttributeEqualsContinually(ImmutableMap.of("timeout", VERY_SHORT_WAIT), entity, Sensors.newStringSensor(Attributes.HTTP_PORT.getName()+".mapped.subnet"), null);
+    }
+    
+    @Test
+    public <T> void testTransformsAllMatchingSensors() throws Exception {
+        AttributeSensor<URI> stronglyTypedUri = Sensors.newSensor(URI.class, "strongly.typed.uri");
+        AttributeSensor<String> stringUri = Sensors.newStringSensor("string.uri");
+        AttributeSensor<URL> stronglyTypedUrl = Sensors.newSensor(URL.class, "strongly.typed.url");
+        AttributeSensor<String> stringUrl = Sensors.newStringSensor("string.url");
+        AttributeSensor<Integer> intPort = Sensors.newIntegerSensor("int.port");
+        AttributeSensor<String> stringPort = Sensors.newStringSensor("string.port");
+        AttributeSensor<HostAndPort> hostAndPort = Sensors.newSensor(HostAndPort.class, "hostAndPort.endpoint");
+        AttributeSensor<String> stringHostAndPort = Sensors.newStringSensor("stringHostAndPort.endpoint");
+
+        entity.sensors().set(Attributes.SUBNET_ADDRESS, privateIp);
+        entity.sensors().set(stronglyTypedUri, URI.create("http://"+publicIp+":1234/my/path"));
+        entity.sensors().set(stringUri, "http://"+publicIp+":1234/my/path");
+        entity.sensors().set(stronglyTypedUrl, new URL("http://"+publicIp+":1234/my/path"));
+        entity.sensors().set(stringUrl, "http://"+publicIp+":1234/my/path");
+        entity.sensors().set(intPort, 1234);
+        entity.sensors().set(stringPort, "1234");
+        entity.sensors().set(hostAndPort, HostAndPort.fromParts(""+publicIp+"", 1234));
+        entity.sensors().set(stringHostAndPort, ""+publicIp+":1234");
+        entity.addLocations(ImmutableList.of(machine));
+        
+        entity.enrichers().add(EnricherSpec.create(OnSubnetNetworkEnricher.class));
+
+        assertAttributeEqualsEventually("strongly.typed.uri.mapped.subnet", "http://"+privateIp+":1234/my/path");
+        assertAttributeEqualsEventually("string.uri.mapped.subnet", "http://"+privateIp+":1234/my/path");
+        assertAttributeEqualsEventually("strongly.typed.url.mapped.subnet", "http://"+privateIp+":1234/my/path");
+        assertAttributeEqualsEventually("string.url.mapped.subnet", "http://"+privateIp+":1234/my/path");
+        assertAttributeEqualsEventually("int.endpoint.mapped.subnet", ""+privateIp+":1234");
+        assertAttributeEqualsEventually("string.endpoint.mapped.subnet", ""+privateIp+":1234");
+        assertAttributeEqualsEventually("hostAndPort.endpoint.mapped.subnet", ""+privateIp+":1234");
+        assertAttributeEqualsEventually("stringHostAndPort.endpoint.mapped.subnet", ""+privateIp+":1234");
+    }
+    
+    @Test
+    public <T> void testIgnoresNonMatchingSensors() throws Exception {
+        AttributeSensor<URI> sensor1 = Sensors.newSensor(URI.class, "my.different");
+        AttributeSensor<URL> sensor2 = Sensors.newSensor(URL.class, "my.different2");
+        AttributeSensor<String> sensor3 = Sensors.newStringSensor("my.different3");
+        AttributeSensor<Integer> sensor4 = Sensors.newIntegerSensor("my.different4");
+        AttributeSensor<HostAndPort> sensor5 = Sensors.newSensor(HostAndPort.class, "my.different5");
+
+        entity.sensors().set(Attributes.SUBNET_ADDRESS, privateIp);
+        entity.sensors().set(sensor1, URI.create("http://"+publicIp+":1234/my/path"));
+        entity.sensors().set(sensor2, new URL("http://"+publicIp+":1234/my/path"));
+        entity.sensors().set(sensor3, "http://"+publicIp+":1234/my/path");
+        entity.sensors().set(sensor4, 1234);
+        entity.sensors().set(sensor5, HostAndPort.fromParts(publicIp, 1234));
+        entity.addLocations(ImmutableList.of(machine));
+        
+        entity.enrichers().add(EnricherSpec.create(OnSubnetNetworkEnricher.class));
+
+        Asserts.succeedsContinually(ImmutableMap.of("timeout", VERY_SHORT_WAIT), new Runnable() {
+            @Override public void run() {
+                Map<AttributeSensor<?>, Object> allSensors = entity.sensors().getAll();
+                String errMsg = "sensors="+allSensors;
+                for (AttributeSensor<?> sensor : allSensors.keySet()) {
+                    String name = sensor.getName();
+                    assertFalse(name.startsWith("my.different") && sensor.getName().contains("subnet"), errMsg);
+                }
+            }});
+    }
+    
+    protected void assertAttributeEqualsEventually(String sensorName, String expectedVal) throws Exception {
+        try {
+            EntityAsserts.assertAttributeEqualsEventually(entity, Sensors.newStringSensor(sensorName), expectedVal);
+        } catch (Exception e) {
+            throw new Exception("Failed assertion for sensor '"+sensorName+"'; attributes are "+entity.sensors().getAll(), e);
+        }
+    }
+    
+    @Test
+    public void testSensorNameConverter() throws Exception {
+        OnSubnetNetworkEnricher enricher = entity.enrichers().add(EnricherSpec.create(OnSubnetNetworkEnricher.class));
+        Function<? super String, String> converter = enricher.getConfig(OnSubnetNetworkEnricher.SENSOR_NAME_CONVERTER);
+        
+        Map<String, String> testCases = ImmutableMap.<String, String>builder()
+                .put("my.uri", "my.uri.mapped.subnet")
+                .put("myuri", "myuri.mapped.subnet")
+                .put("my.UrI", "my.UrI.mapped.subnet")
+                .put("my.url", "my.url.mapped.subnet")
+                .put("myurl", "myurl.mapped.subnet")
+                .put("my.endpoint", "my.endpoint.mapped.subnet")
+                .put("myendpoint", "myendpoint.mapped.subnet")
+                .put("my.port", "my.endpoint.mapped.subnet")
+                .put("myport", "my.endpoint.mapped.subnet")
+                .build();
+        
+        for (Map.Entry<String, String> entry : testCases.entrySet()) {
+            assertEquals(converter.apply(entry.getKey()), entry.getValue(), "input="+entry.getKey());
+        }
+    }
+    
+    @Test(expectedExceptions=IllegalStateException.class, expectedExceptionsMessageRegExp=".*must not have both 'sensor' and 'sensors'.*")
+    public void testFailsIfSensorAndSensorsConfigured() throws Exception {
+        entity.enrichers().add(EnricherSpec.create(OnSubnetNetworkEnricher.class)
+                .configure(OnSubnetNetworkEnricher.SENSOR, Attributes.HTTP_PORT)
+                .configure(OnSubnetNetworkEnricher.SENSORS, ImmutableList.of(Attributes.HTTPS_PORT)));
+    }
+    
+    @Test(expectedExceptions=IllegalStateException.class, expectedExceptionsMessageRegExp=".*must not have explicit 'mapMatching', and either of 'sensor' or 'sensors'.*")
+    public void testFailsIfSensorAndMapMatchingConfigured() throws Exception {
+        entity.enrichers().add(EnricherSpec.create(OnSubnetNetworkEnricher.class)
+                .configure(OnSubnetNetworkEnricher.SENSOR, Attributes.HTTP_PORT)
+                .configure(OnSubnetNetworkEnricher.MAP_MATCHING, ".*uri"));
+    }
+    
+    @Test(expectedExceptions=IllegalStateException.class, expectedExceptionsMessageRegExp=".*must not have explicit 'mapMatching', and either of 'sensor' or 'sensors'.*")
+    public void testFailsIfSensorsAndMapMatchingConfigured() throws Exception {
+        entity.enrichers().add(EnricherSpec.create(OnSubnetNetworkEnricher.class)
+                .configure(OnSubnetNetworkEnricher.SENSORS, ImmutableList.of(Attributes.HTTPS_PORT))
+                .configure(OnSubnetNetworkEnricher.MAP_MATCHING, ".*uri"));
+    }
+}


Mime
View raw message