add primitive-or-object deserializer, fix bug resolving entity exprs in triggers
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WorkflowYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WorkflowYamlTest.java
index 7519a37..fb1b100 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WorkflowYamlTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WorkflowYamlTest.java
@@ -18,6 +18,16 @@
  */
 package org.apache.brooklyn.camp.brooklyn;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
 import com.google.common.base.Stopwatch;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -38,7 +48,13 @@
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslUtils;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.BrooklynDslCommon;
 import org.apache.brooklyn.core.config.ConfigKeys;
-import org.apache.brooklyn.core.entity.*;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
+import org.apache.brooklyn.core.entity.Dumper;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.entity.EntityAdjuncts;
+import org.apache.brooklyn.core.entity.EntityAsserts;
+import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
 import org.apache.brooklyn.core.entity.trait.Startable;
 import org.apache.brooklyn.core.sensor.Sensors;
@@ -48,7 +64,12 @@
 import org.apache.brooklyn.core.typereg.BasicTypeImplementationPlan;
 import org.apache.brooklyn.core.typereg.JavaClassNameTypePlanTransformer;
 import org.apache.brooklyn.core.typereg.RegisteredTypes;
-import org.apache.brooklyn.core.workflow.*;
+import org.apache.brooklyn.core.workflow.WorkflowBasicTest;
+import org.apache.brooklyn.core.workflow.WorkflowEffector;
+import org.apache.brooklyn.core.workflow.WorkflowExecutionContext;
+import org.apache.brooklyn.core.workflow.WorkflowPolicy;
+import org.apache.brooklyn.core.workflow.WorkflowStepDefinition;
+import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
 import org.apache.brooklyn.core.workflow.steps.flow.LogWorkflowStep;
 import org.apache.brooklyn.core.workflow.store.WorkflowStatePersistenceViaSensors;
 import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess;
@@ -74,16 +95,6 @@
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.time.temporal.TemporalUnit;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
 import static org.apache.brooklyn.util.core.internal.ssh.ExecCmdAsserts.assertExecContains;
 import static org.apache.brooklyn.util.core.internal.ssh.ExecCmdAsserts.assertExecsContain;
 import static org.testng.Assert.assertTrue;
@@ -291,6 +302,17 @@
         doTestWorkflowPolicy("condition: { sensor: not_exist }\n" + "period: 200 ms", null);
     }
 
+    @Test
+    public void testWorkflowPolicyTriggersWithEntityId() throws Exception {
+        doTestWorkflowPolicy("triggers: [ { sensor: theTrigger, entity: other_entity } ]", Duration.seconds(1)::isLongerThan, null, true);
+    }
+    @Test
+    public void testWorkflowPolicyTriggersWithEntityInstance() throws Exception {
+        // see org.apache.brooklyn.core.resolve.jackson.BrooklynMiscJacksonSerializationTest.testPrimitiveWithObjectForEntity
+        doTestWorkflowPolicy("triggers: [ { sensor: theTrigger, entity: $brooklyn:entity(\"other_entity\") } ]", Duration.seconds(1)::isLongerThan, null, true);
+    }
+
+
     static final AttributeSensor<Object> MY_WORKFLOW_SENSOR = Sensors.newSensor(Object.class, "myWorkflowSensor");
 
     Entity doTestWorkflowSensor(String triggers, Predicate<Duration> timeCheckOrNullIfShouldFail) throws Exception {
@@ -350,9 +372,13 @@
     }
 
     public void doTestWorkflowPolicy(String triggers, Predicate<Duration> timeCheckOrNullIfShouldFail, Consumer<Policy> extraChecks) throws Exception {
+        doTestWorkflowPolicy(triggers, timeCheckOrNullIfShouldFail, extraChecks, false);
+    }
+    public void doTestWorkflowPolicy(String triggers, Predicate<Duration> timeCheckOrNullIfShouldFail, Consumer<Policy> extraChecks, boolean useOtherEntity) throws Exception {
         Entity app = createAndStartApplication(
                 "services:",
                 "- type: " + BasicEntity.class.getName(),
+                "  id: main_entity",
                 "  brooklyn.policies:",
                 "  - type: workflow-policy",
                 "    brooklyn.config:",
@@ -370,13 +396,17 @@
                 "            v: ${v}",
                 "        - transform map x = ${out} | yaml",
                 "        - set-sensor myWorkflowSensor = ${x}",
+                "- type: " + BasicEntity.class.getName(),
+                "  id: other_entity",
                 "");
 
         Stopwatch sw = Stopwatch.createStarted();
         waitForApplicationTasks(app);
         Duration d1 = Duration.of(sw);
 
-        Entity entity = Iterables.getOnlyElement(app.getChildren());
+        Iterator<Entity> ci = app.getChildren().iterator();
+        Entity entity = ci.next();
+        Entity otherEntity = ci.next();
         Policy policy = entity.policies().asList().stream().filter(p -> p instanceof WorkflowPolicy).findAny().get();
         Asserts.assertEquals(policy.getDisplayName(), "Set myWorkflowSensor");
         // should really ID be settable from flag?
@@ -390,7 +420,7 @@
             Asserts.assertThat(d2, Duration.millis(500)::isLongerThan);
 //            EntityAsserts.assertAttributeEqualsEventually(entity, s, MutableMap.of("foo", "bar", "v", 0));
 
-            entity.sensors().set(Sensors.newStringSensor("theTrigger"), "go");
+            (useOtherEntity ? otherEntity : entity).sensors().set(Sensors.newStringSensor("theTrigger"), "go");
             EntityAsserts.assertAttributeEqualsEventually(MutableMap.of("timeout", "5s"), entity, MY_WORKFLOW_SENSOR, MutableMap.of("foo", "bar", "v", 0));
 //            EntityAsserts.assertAttributeEqualsEventually(entity, s, MutableMap.of("foo", "bar", "v", 1));
             Duration d3 = Duration.of(sw).subtract(d2);
diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/AsPropertyIfAmbiguous.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/AsPropertyIfAmbiguous.java
index 6824985..b266cd2 100644
--- a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/AsPropertyIfAmbiguous.java
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/AsPropertyIfAmbiguous.java
@@ -267,7 +267,9 @@
                 boolean canTryWithoutType = !typeIdFindResult.isUnambiguous;
                 try {
                     Object result = _deserializeTypedForId(p, ctxt, tb, typeIdFindResult.type);
-                    if (_idResolver instanceof HasBaseType) {
+                    if (result==null) {
+                        LOG.trace("Null result deserializing");
+                    } else if (_idResolver instanceof HasBaseType) {
                         JavaType baseType = ((HasBaseType) _idResolver).getBaseType();
                         if (baseType != null) {
                             Class<?> rawClass = baseType.getRawClass();
diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/CommonTypesSerialization.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/CommonTypesSerialization.java
index fb5e553..f294ee8 100644
--- a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/CommonTypesSerialization.java
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/CommonTypesSerialization.java
@@ -31,15 +31,21 @@
 import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
 import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
 import com.google.common.reflect.TypeToken;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.mgmt.EntityManager;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
 import org.apache.brooklyn.api.objs.BrooklynObject;
 import org.apache.brooklyn.api.objs.BrooklynObjectType;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
+import org.apache.brooklyn.core.mgmt.internal.EntityManagerInternal;
+import org.apache.brooklyn.core.mgmt.internal.NonDeploymentManagementContext;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.core.flags.BrooklynTypeNameResolution;
 import org.apache.brooklyn.util.core.flags.FlagUtils;
 import org.apache.brooklyn.util.core.predicates.DslPredicates;
+import org.apache.brooklyn.util.core.task.Tasks;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.javalang.Boxing;
 import org.apache.brooklyn.util.time.Duration;
@@ -356,6 +362,17 @@
             // we could support 'current' to use tasks to resolve, which might be handy
             BrooklynObject result = mgmt.lookup(value);
             if (result!=null) return result;
+
+            Entity currentEntity = BrooklynTaskTags.getContextEntity(Tasks.current());
+            if (currentEntity!=null) {
+                // during initialization, we can look relative to ourselves, since entities aren't available in mgmt.lookup
+                Iterable<Entity> ents = ((EntityManagerInternal) mgmt.getEntityManager()).getAllEntitiesInApplication(currentEntity.getApplication());
+                for (Entity e : ents) {
+                    if (Objects.equals(value, e.getId())) {
+                        return e;
+                    }
+                }
+            }
             throw new IllegalStateException("Entity or other BrooklynObject '"+value+"' is not known here");
         }
 
@@ -443,7 +460,11 @@
                     return super.convertStringToObject(value, p, ctxt);
                 } catch (Exception e) {
                     Exceptions.propagateIfFatal(e);
-                    LOG.warn("Reference to BrooklynObject "+value+" which is unknown or no longer available; replacing with 'null'");
+                    if (BrooklynObjectSerialization.this.mgmt instanceof NonDeploymentManagementContext) {
+                        LOG.warn("Reference to BrooklynObject " + value + " which is unknown or not yet known, using NonDeployment context; replacing with 'null': "+e);
+                    } else {
+                        LOG.warn("Reference to BrooklynObject " + value + " which is unknown or no longer available; replacing with 'null': "+e);
+                    }
                     return null;
                 }
             }
diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/JsonSymbolDependentDeserializer.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/JsonSymbolDependentDeserializer.java
index 93d8b7f..5b2c5ff 100644
--- a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/JsonSymbolDependentDeserializer.java
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/JsonSymbolDependentDeserializer.java
@@ -29,6 +29,8 @@
 import java.io.IOException;
 import java.util.Set;
 import java.util.function.Function;
+
+import org.apache.brooklyn.util.core.units.Range;
 import org.apache.brooklyn.util.core.xstream.ImmutableSetConverter;
 
 import static org.apache.brooklyn.core.resolve.jackson.BrooklynJacksonSerializationUtils.createBeanDeserializer;
@@ -152,5 +154,4 @@
                 /** try to do low level build so we don't recreate ourselves and loop endlessly */ true,
                 true);
     }
-
 }
diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/PrimitiveTokenOrExpectedObject.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/PrimitiveTokenOrExpectedObject.java
new file mode 100644
index 0000000..ff342c2
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/PrimitiveTokenOrExpectedObject.java
@@ -0,0 +1,109 @@
+/*
+ * 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.resolve.jackson;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.apache.brooklyn.core.resolve.jackson.PrimitiveTokenOrExpectedObject.PrimitiveTokenOrExpectedObjectDeserializer;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.guava.Maybe;
+
+import static org.apache.brooklyn.core.resolve.jackson.BrooklynJacksonSerializationUtils.createBeanDeserializer;
+
+/**
+ * This is an object intended for use in Jackson setters. When supplied with a primitive token, the value is stored as the primitive.
+ * For anything else, it is deserialized according to the type T.
+ * Thus setters can inspect the object and call the right method, and a custom string can be handled specially without need for lower-level Jackson interrogation,
+ * but if the object, or a map of the object is supplied, it is used.
+ */
+@JsonDeserialize(using = PrimitiveTokenOrExpectedObjectDeserializer.class)
+public class PrimitiveTokenOrExpectedObject<T> {
+
+    // exactly one of these will be set
+    public T object;
+    public Object primitive;
+
+    public boolean hasObject() { return object!=null; }
+    public boolean hasPrimitive() { return primitive!=null; }
+    public boolean hasStringPrimitive() { return primitive instanceof String; }
+
+    public T asObject() { return object; }
+    public Object asPrimitive() { return primitive; }
+    public String asString() { if (hasStringPrimitive()) return (String)primitive; return null; }
+
+    public static class PrimitiveTokenOrExpectedObjectDeserializer extends JsonSymbolDependentDeserializer {
+        public PrimitiveTokenOrExpectedObjectDeserializer() {
+            super();
+        }
+        @Override
+        public JavaType getDefaultType() {
+            return ctxt.constructType(Object.class);
+        }
+
+        protected Maybe<Object> getTokenValue(JsonToken token, JsonParser p) {
+            try {
+                if (SIMPLE_TOKENS.contains(token)) {
+                    if (JsonToken.VALUE_STRING.equals(token)) return Maybe.of(p.getValueAsString());
+                    if (JsonToken.VALUE_NUMBER_INT.equals(token)) return Maybe.of(p.getValueAsInt());
+                    if (JsonToken.VALUE_NUMBER_FLOAT.equals(token)) return Maybe.of(p.getValueAsDouble());
+                    if (token.isBoolean()) return Maybe.of(p.getValueAsBoolean());
+                    if (JsonToken.VALUE_NULL.equals(token)) return Maybe.ofAllowingNull(null);
+                }
+                return Maybe.absent();
+            } catch (IOException e) {
+                throw Exceptions.propagate(e);
+            }
+        }
+        @Override
+        protected Object deserializeToken(JsonParser p) throws IOException {
+            PrimitiveTokenOrExpectedObject result = new PrimitiveTokenOrExpectedObject();
+            result.primitive = getTokenValue(p.getCurrentToken(), p).get();
+            return result;
+        }
+
+        @Override
+        protected Object deserializeObject(JsonParser p) throws IOException {
+            PrimitiveTokenOrExpectedObject result = new PrimitiveTokenOrExpectedObject();
+            result.object = super.deserializeObject(p);
+            return result;
+        }
+
+        protected JsonDeserializer<?> getObjectDeserializer() throws IOException, JsonProcessingException {
+            if (type!=null && PrimitiveTokenOrExpectedObject.class.equals(type.getRawClass())) {
+                // this should always happen
+                return ctxt.findRootValueDeserializer(type.containedType(0));
+            } else {
+                return super.getObjectDeserializer();
+            }
+        }
+
+        public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
+            return super.createContextual(ctxt, property);
+        }
+    }
+}
diff --git a/core/src/main/java/org/apache/brooklyn/core/sensor/AbstractAddTriggerableSensor.java b/core/src/main/java/org/apache/brooklyn/core/sensor/AbstractAddTriggerableSensor.java
index f9adc8a..a5c2c5a 100644
--- a/core/src/main/java/org/apache/brooklyn/core/sensor/AbstractAddTriggerableSensor.java
+++ b/core/src/main/java/org/apache/brooklyn/core/sensor/AbstractAddTriggerableSensor.java
@@ -26,6 +26,7 @@
 import java.util.stream.Collectors;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonSetter;
 import com.google.common.annotations.Beta;
 import com.google.common.base.Predicates;
 import com.google.common.reflect.TypeToken;
@@ -38,6 +39,7 @@
 import org.apache.brooklyn.core.entity.EntityPredicates;
 import org.apache.brooklyn.core.feed.PollConfig;
 import org.apache.brooklyn.core.mgmt.internal.AppGroupTraverser;
+import org.apache.brooklyn.core.resolve.jackson.PrimitiveTokenOrExpectedObject;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.core.config.ConfigBag;
 import org.apache.brooklyn.util.core.predicates.DslPredicates;
@@ -105,26 +107,28 @@
                     t = new SensorFeedTrigger();
                     t.sensorName = (String) ti;
                 } else {
-                    throw new IllegalStateException("Trigger should be a map specifyin entity and sensor");
+                    throw new IllegalStateException("Trigger should be a map specifying entity and sensor");
                 }
             }
 
             Entity entity = t.entity;
-            if (entity==null && t.entityId!=null) {
-                String desiredComponentId = t.entityId;
-                List<Entity> firstGroupOfMatches = AppGroupTraverser.findFirstGroupOfMatches(context, true,
-                        Predicates.and(EntityPredicates.configEqualTo(BrooklynConfigKeys.PLAN_ID, desiredComponentId), x->true)::apply);
-                if (firstGroupOfMatches.isEmpty()) {
-                    firstGroupOfMatches = AppGroupTraverser.findFirstGroupOfMatches(context, true,
-                            Predicates.and(EntityPredicates.idEqualTo(desiredComponentId), x->true)::apply);
-                }
-                if (!firstGroupOfMatches.isEmpty()) {
-                    entity = firstGroupOfMatches.get(0);
+            if (entity==null) {
+                if (t.entityId != null) {
+                    String desiredComponentId = t.entityId;
+                    List<Entity> firstGroupOfMatches = AppGroupTraverser.findFirstGroupOfMatches(context, true,
+                            Predicates.and(EntityPredicates.configEqualTo(BrooklynConfigKeys.PLAN_ID, desiredComponentId), x -> true)::apply);
+                    if (firstGroupOfMatches.isEmpty()) {
+                        firstGroupOfMatches = AppGroupTraverser.findFirstGroupOfMatches(context, true,
+                                Predicates.and(EntityPredicates.idEqualTo(desiredComponentId), x -> true)::apply);
+                    }
+                    if (!firstGroupOfMatches.isEmpty()) {
+                        entity = firstGroupOfMatches.get(0);
+                    } else {
+                        throw new IllegalStateException("Cannot find entity with ID '" + desiredComponentId + "'");
+                    }
                 } else {
-                    throw new IllegalStateException("Cannot find entity with ID '"+desiredComponentId+"'");
+                    entity = context;
                 }
-            } else {
-                entity = context;
             }
 
             Sensor sensor = t.sensor;
@@ -157,6 +161,17 @@
 
         // could support predicates on the value; but we do it on the entity which is enough
 
+        @JsonSetter
+//        @JsonDeserialize(using = PrimitiveOrObjectDeserializer.class)
+//        @JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
+        public void setEntity(PrimitiveTokenOrExpectedObject<Entity> po) {
+                //@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) @JsonDeserialize(using = PrimitiveOrObjectDeserializer.class)
+//                                      Object entity) {
+            if (po.hasObject()) setEntity(po.asObject());
+            else if (po.hasStringPrimitive()) setEntity(po.asString());
+            else if (entity==null) { /* do nothing */ }
+            else throw new IllegalArgumentException("Invalid input for entity to "+this+": "+entity);
+        }
         public void setEntity(Entity entity) {
             this.entity = entity;
         }
diff --git a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java
index 4e74a96..fb38bb9 100644
--- a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java
@@ -18,6 +18,15 @@
  */
 package org.apache.brooklyn.core.resolve.jackson;
 
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.function.BiConsumer;
+
 import com.fasterxml.jackson.core.JacksonException;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -28,8 +37,12 @@
 import com.fasterxml.jackson.databind.util.TokenBuffer;
 import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
 import com.google.common.reflect.TypeToken;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.sensor.AbstractAddTriggerableSensor.SensorFeedTrigger;
 import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.entity.stock.BasicApplicationImpl;
 import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
@@ -47,11 +60,6 @@
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import java.io.IOException;
-import java.time.Instant;
-import java.util.*;
-import java.util.function.BiConsumer;
-
 public class BrooklynMiscJacksonSerializationTest implements MapperTestFixture {
 
     private static final Logger LOG = LoggerFactory.getLogger(BrooklynMiscJacksonSerializationTest.class);
@@ -59,7 +67,7 @@
     private ObjectMapper mapper;
 
     public ObjectMapper mapper() {
-        if (mapper==null) mapper = BeanWithTypeUtils.newMapper(null, false, null, true);
+        if (mapper == null) mapper = BeanWithTypeUtils.newMapper(null, false, null, true);
         return mapper;
     }
 
@@ -70,12 +78,13 @@
 
     // baseline
 
-    static class EmptyObject {}
+    static class EmptyObject {
+    }
 
     @Test
     public void testMapperDoesntBreakBasicThings() throws Exception {
         Asserts.assertEquals(deser("\"hello\""), "hello");
-        Asserts.assertInstanceOf(deser("{\"type\":\""+EmptyObject.class.getName()+"\"}"), EmptyObject.class);
+        Asserts.assertInstanceOf(deser("{\"type\":\"" + EmptyObject.class.getName() + "\"}"), EmptyObject.class);
     }
 
     @Test
@@ -90,7 +99,7 @@
         public String toString() {
             return "Obj{" +
                     "foo='" + foo + '\'' +
-                    "}@"+ System.identityHashCode(this);
+                    "}@" + System.identityHashCode(this);
         }
     }
 
@@ -100,37 +109,42 @@
         mapper = BeanWithTypeUtils.applyCommonMapperConfig(mapper, null, false, null, true);
         mapper = new ObjectReferencingSerialization().useAndApplytoMapper(mapper);
 
-        ObjForSerializingAsReference f1 = new ObjForSerializingAsReference(); f1.foo = "1";
-        ObjForSerializingAsReference f2 = new ObjForSerializingAsReference(); f2.foo = "2";
+        ObjForSerializingAsReference f1 = new ObjForSerializingAsReference();
+        f1.foo = "1";
+        ObjForSerializingAsReference f2 = new ObjForSerializingAsReference();
+        f2.foo = "2";
         String out = ser(MutableMap.of("a", f1, "b", f2, "c", f1));
-        LOG.info("Result of "+ JavaClassNames.niceClassAndMethod()+": "+out);
+        LOG.info("Result of " + JavaClassNames.niceClassAndMethod() + ": " + out);
 
         Map in = deser(out,
                 Map.class
 //                new TypeToken<Map<String, ObjForSerializingAsReference>>() {}
         );
-        ObjForSerializingAsReference a = (ObjForSerializingAsReference)in.get("a");
-        ObjForSerializingAsReference b = (ObjForSerializingAsReference)in.get("b");
-        ObjForSerializingAsReference c = (ObjForSerializingAsReference)in.get("c");
-        Asserts.assertTrue(a.foo.equals(c.foo), "expected same foo value for a and c - "+a+" != "+c);
+        ObjForSerializingAsReference a = (ObjForSerializingAsReference) in.get("a");
+        ObjForSerializingAsReference b = (ObjForSerializingAsReference) in.get("b");
+        ObjForSerializingAsReference c = (ObjForSerializingAsReference) in.get("c");
+        Asserts.assertTrue(a.foo.equals(c.foo), "expected same foo value for a and c - " + a + " != " + c);
         Asserts.assertTrue(!b.foo.equals(c.foo), "expected different foo value for a and b");
-        Asserts.assertTrue(a == c, "expected same instance for a and c - "+a+" != "+c);
+        Asserts.assertTrue(a == c, "expected same instance for a and c - " + a + " != " + c);
         Asserts.assertTrue(a != b, "expected different instance for a and b");
     }
 
     @Test
     public void testObjectReferences() throws IOException {
-        ObjForSerializingAsReference f1 = new ObjForSerializingAsReference(); f1.foo = "1";
+        ObjForSerializingAsReference f1 = new ObjForSerializingAsReference();
+        f1.foo = "1";
         Object f2 = new ObjectReferencingSerialization().serializeAndDeserialize(f1);
         Asserts.assertEquals(f1, f2);
-        Asserts.assertTrue(f1==f2, "different instances for "+f1+" and "+f2);
+        Asserts.assertTrue(f1 == f2, "different instances for " + f1 + " and " + f2);
     }
 
     public static class ObjRefAcceptingStringSource {
         String src;
         ObjRefAcceptingStringSource bar;
 
-        public ObjRefAcceptingStringSource() {}
+        public ObjRefAcceptingStringSource() {
+        }
+
         public ObjRefAcceptingStringSource(String src) {
             this.src = src;
         }
@@ -141,16 +155,16 @@
         ManagementContext mgmt = LocalManagementContextForTests.newInstance();
 
         Asserts.assertFailsWith(() -> {
-                ObjRefAcceptingStringSource b = BeanWithTypeUtils.convertShallow(mgmt, MutableMap.of("bar", new DeferredSupplier() {
-                    @Override
-                    public Object get() {
-                        return "xxx";
-                    }
-                }), TypeToken.of(ObjRefAcceptingStringSource.class), false, null, true);
-                // ensure the ID of a serialized object isn't treated as a reference
-                Asserts.fail("Should have failed, instead got: " + b.bar.src);
-                return b;
-            }, e -> Asserts.expectedFailureContains(e, "Problem deserializing property 'bar'"));
+            ObjRefAcceptingStringSource b = BeanWithTypeUtils.convertShallow(mgmt, MutableMap.of("bar", new DeferredSupplier() {
+                @Override
+                public Object get() {
+                    return "xxx";
+                }
+            }), TypeToken.of(ObjRefAcceptingStringSource.class), false, null, true);
+            // ensure the ID of a serialized object isn't treated as a reference
+            Asserts.fail("Should have failed, instead got: " + b.bar.src);
+            return b;
+        }, e -> Asserts.expectedFailureContains(e, "Problem deserializing property 'bar'"));
 
         ObjRefAcceptingStringSource b = BeanWithTypeUtils.convertShallow(mgmt, MutableMap.of("bar", new ObjRefAcceptingStringSource("good")), TypeToken.of(ObjRefAcceptingStringSource.class), false, null, true);
         Asserts.assertEquals(b.bar.src, "good");
@@ -185,7 +199,7 @@
     public static class DateTimeBean {
         String x;
         Date juDate;
-//        LocalDateTime localDateTime;
+        //        LocalDateTime localDateTime;
         GregorianCalendar calendar;
         Instant instant;
     }
@@ -196,11 +210,11 @@
 //        mapper.findAndRegisterModules();
 
         DateTimeBean impl = new DateTimeBean();
-        Asserts.assertEquals(ser(impl, DateTimeBean.class), "{}" );
+        Asserts.assertEquals(ser(impl, DateTimeBean.class), "{}");
 
         impl.x = "foo";
 
-        impl.juDate = new Date(60*1000);
+        impl.juDate = new Date(60 * 1000);
 //        impl.localDateTime = LocalDateTime.of(2020, 1, 1, 12, 0, 0, 0);
         impl.calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"), Locale.ROOT);
         impl.calendar.set(2020, 0, 1, 12, 0, 0);
@@ -222,18 +236,18 @@
                 "instant: 2020-01-01T12:00:00Z",
                 ""
         ), DateTimeBean.class);
-        Assert.assertEquals( impl2.x, impl.x );
-        Assert.assertEquals( impl2.juDate, impl.juDate );
+        Assert.assertEquals(impl2.x, impl.x);
+        Assert.assertEquals(impl2.juDate, impl.juDate);
 //        Assert.assertEquals( impl2.localDateTime, impl.localDateTime );
 //        Assert.assertEquals( impl2.calendar, impl.calendar );
-        Assert.assertEquals( impl2.instant, impl.instant );
+        Assert.assertEquals(impl2.instant, impl.instant);
     }
 
     @Test
     public void testInstantConversionFromVarious() throws Exception {
         mapper = BeanWithTypeUtils.newYamlMapper(null, false, null, true);
         long utc = new Date().getTime();
-        Instant inst = mapper.readerFor(Instant.class).readValue( mapper.writeValueAsString(utc) );
+        Instant inst = mapper.readerFor(Instant.class).readValue(mapper.writeValueAsString(utc));
         // below known not to work, as long is converted to ["j...Long", utc] which we don't process
         //mapper.readerFor(Instant.class).readValue( mapper.writerFor(Object.class).writeValueAsString(utc) );
 
@@ -242,8 +256,8 @@
         BeanWithTypeUtils.convertShallow(mgmt, utc, TypeToken.of(Instant.class), false, null, false);
         BeanWithTypeUtils.convertDeeply(mgmt, utc, TypeToken.of(Instant.class), false, null, false);
 
-        BeanWithTypeUtils.convertShallow(mgmt, ""+utc, TypeToken.of(Instant.class), false, null, false);
-        BeanWithTypeUtils.convertDeeply(mgmt, ""+utc, TypeToken.of(Instant.class), false, null, false);
+        BeanWithTypeUtils.convertShallow(mgmt, "" + utc, TypeToken.of(Instant.class), false, null, false);
+        BeanWithTypeUtils.convertDeeply(mgmt, "" + utc, TypeToken.of(Instant.class), false, null, false);
 
         BeanWithTypeUtils.convertShallow(mgmt, inst, TypeToken.of(Instant.class), false, null, false);
         BeanWithTypeUtils.convertDeeply(mgmt, inst, TypeToken.of(Instant.class), false, null, false);
@@ -270,7 +284,7 @@
     public void testStringBean() throws Exception {
         Duration d = mapper().readValue("\"1m\"", Duration.class);
         Asserts.assertEquals(d, Duration.ONE_MINUTE);
-        Object d0 = mapper().readValue("{\"type\":\""+Duration.class.getName()+"\",\"value\":\"1s\"}", Object.class);
+        Object d0 = mapper().readValue("{\"type\":\"" + Duration.class.getName() + "\",\"value\":\"1s\"}", Object.class);
         Asserts.assertEquals(d0, Duration.ONE_SECOND);
     }
 
@@ -282,7 +296,7 @@
 
     @Test
     public void testJsonPassThrough() throws Exception {
-        BiConsumer<String,Object> check = (input, expected) -> {
+        BiConsumer<String, Object> check = (input, expected) -> {
             try {
                 JsonPassThroughDeserializer.JsonObjectHolder x = mapper().readValue(input, JsonPassThroughDeserializer.JsonObjectHolder.class);
                 Asserts.assertEquals(x.value, expected);
@@ -297,7 +311,7 @@
         check.accept("42", 42);
         check.accept("{\"k\":\"v\"}", MutableMap.of("k", "v"));
         check.accept("[\"a\",1]", MutableList.of("a", 1));
-        check.accept("[\"a\",{\"type\":\""+Duration.class.getName()+"\",\"value\":\"1s\"}]",
+        check.accept("[\"a\",{\"type\":\"" + Duration.class.getName() + "\",\"value\":\"1s\"}]",
                 MutableList.of("a", MutableMap.of("type", Duration.class.getName(), "value", "1s")));
     }
 
@@ -311,11 +325,17 @@
         public Object subtypeWanted;
         public String x;
     }
+
     @JsonDeserialize(using = JsonDeserializer.None.class)
-    static class SampleFromStringSubtype extends SampleFromStringDeserialized {}
+    static class SampleFromStringSubtype extends SampleFromStringDeserialized {
+    }
+
     @JsonDeserialize(using = JsonDeserializer.None.class)
-    static class SampleFromStringSubtype1 extends SampleFromStringSubtype {}
-    static class SampleFromStringSubtype2 extends SampleFromStringSubtype {}
+    static class SampleFromStringSubtype1 extends SampleFromStringSubtype {
+    }
+
+    static class SampleFromStringSubtype2 extends SampleFromStringSubtype {
+    }
 
     static class SampleFromStringDeserializer extends JsonDeserializer {
         @Override
@@ -326,13 +346,13 @@
                     ctxt);
 
             Integer rawi = null;
-            if (raw instanceof String) rawi = Integer.parseInt((String)raw);
-            if (raw instanceof Integer) rawi = (Integer)raw;
-            if (rawi!=null) {
+            if (raw instanceof String) rawi = Integer.parseInt((String) raw);
+            if (raw instanceof Integer) rawi = (Integer) raw;
+            if (rawi != null) {
                 SampleFromStringSubtype result = null;
-                if (rawi==1) result = new SampleFromStringSubtype1();
-                if (rawi==2) result = new SampleFromStringSubtype2();
-                if (result!=null) {
+                if (rawi == 1) result = new SampleFromStringSubtype1();
+                if (rawi == 2) result = new SampleFromStringSubtype2();
+                if (result != null) {
                     result.subtypeWanted = raw;
                 }
                 return result;
@@ -340,9 +360,9 @@
 
             if (raw instanceof Map) {
                 Integer stw = TypeCoercions.tryCoerce(((Map) raw).get("subtypeWanted"), Integer.class).orNull();
-                if (stw!=null && stw>=0) {
+                if (stw != null && stw >= 0) {
                     try {
-                        return ctxt.findNonContextualValueDeserializer(ctxt.constructType(Class.forName(SampleFromStringSubtype.class.getName()+stw))).deserialize(
+                        return ctxt.findNonContextualValueDeserializer(ctxt.constructType(Class.forName(SampleFromStringSubtype.class.getName() + stw))).deserialize(
                                 BrooklynJacksonSerializationUtils.createParserFromTokenBufferAndParser(buffer, p), ctxt);
                     } catch (ClassNotFoundException e) {
                         throw new RuntimeException(e);
@@ -361,25 +381,41 @@
         mapper = BeanWithTypeUtils.newSimpleYamlMapper();  //YAMLMapper.builder().build();
         s = mapper.readerFor(SampleFromStringDeserialized.class).readValue("1");
         Asserts.assertInstanceOf(s, SampleFromStringSubtype1.class);
-        Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, 1 );
+        Asserts.assertEquals(((SampleFromStringSubtype) s).subtypeWanted, 1);
 
         s = mapper.readerFor(SampleFromStringDeserialized.class).readValue("\"2\"");
         Asserts.assertInstanceOf(s, SampleFromStringSubtype2.class);
-        Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, "2" );
+        Asserts.assertEquals(((SampleFromStringSubtype) s).subtypeWanted, "2");
 
         s = mapper.readerFor(SampleFromStringDeserialized.class).readValue("subtypeWanted: 1");
         Asserts.assertInstanceOf(s, SampleFromStringSubtype1.class);
-        Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, 1 );
+        Asserts.assertEquals(((SampleFromStringSubtype) s).subtypeWanted, 1);
 
         s = mapper.readerFor(SampleFromStringDeserialized.class).readValue("subtypeWanted: \"-1\"");
         Asserts.assertEquals(s.getClass(), SampleFromStringSubtype.class);
-        Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, "-1" );
+        Asserts.assertEquals(((SampleFromStringSubtype) s).subtypeWanted, "-1");
 
         s = TypeCoercions.coerce("1", SampleFromStringDeserialized.class);
         Asserts.assertInstanceOf(s, SampleFromStringSubtype1.class);
-        Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, "1" );
+        Asserts.assertEquals(((SampleFromStringSubtype) s).subtypeWanted, "1");
         s = TypeCoercions.coerce(1, SampleFromStringDeserialized.class);
-        Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, 1 );
+        Asserts.assertEquals(((SampleFromStringSubtype) s).subtypeWanted, 1);
     }
 
-}
+    @Test
+    public void testPrimitiveWithObjectForEntity() throws Exception {
+        ManagementContext mgmt = LocalManagementContextForTests.newInstance();
+        Entity app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplicationImpl.class));
+
+        Asserts.assertThat(BeanWithTypeUtils.convert(mgmt,
+                        MutableMap.of("entity", app, "sensor", "aTrigger"),
+                        TypeToken.of(SensorFeedTrigger.class), true, null, true),
+                f -> f != null && app.equals(f.getEntity()) && f.getSensor().equals("aTrigger"));
+
+        Asserts.assertThat(BeanWithTypeUtils.convert(mgmt,
+                        MutableMap.of("entity", app.getApplicationId(), "sensor", "aTrigger"),
+                        TypeToken.of(SensorFeedTrigger.class), true, null, true),
+                f -> f != null && app.getId().equals(f.getEntity()) && f.getSensor().equals("aTrigger"));
+    }
+
+}
\ No newline at end of file