TINKERPOP-2235 Consistent behavior for multi/meta properties and null
diff --git a/docs/src/upgrade/release-3.5.x.asciidoc b/docs/src/upgrade/release-3.5.x.asciidoc
index 841176d..ebe5456 100644
--- a/docs/src/upgrade/release-3.5.x.asciidoc
+++ b/docs/src/upgrade/release-3.5.x.asciidoc
@@ -363,15 +363,24 @@
 Graph providers should take note of the changes to `null` semantics described in the "users" section of these upgrade
 notes. As `null` is now acceptable as a `Traverser` object, this change may affect custom steps. Further note that
 `null` now works more consistently with mutation steps and graph providers may need to include additional logic to
-deal with those possible conditions. Please see the console session below which uses TinkerGraph to demonstrate the
-current behavioral expectations:
+deal with those possible conditions. Please see the console sessions below which uses TinkerGraph to demonstrate the
+current behavioral expectations.
 
 [source,text]
 ----
+gremlin> g.getGraph().features().vertex().supportsNullPropertyValues()
+==>false
 gremlin> g.addV(null).property(id, null).property('name',null)
 ==>v[0]
 gremlin> g.V().elementMap()
 ==>[id:0,label:vertex]
+...
+gremlin> g.getGraph().features().vertex().supportsNullPropertyValues()
+==>true
+gremlin> g.addV(null).property(id, null).property('name',null)
+==>v[0]
+gremlin> g.V().elementMap()
+==>[id:0,label:vertex,name:null]
 ----
 
 In the above example, `addV()` defaults to `Vertex.DEFAULT_LABEL`, the `id` is generated and setting the "name"
@@ -380,14 +389,25 @@
 
 [source,text]
 ----
-gremlin> g.V().property('name','stephen')
+gremlin> g.getGraph().features().vertex().supportsNullPropertyValues()
+==>false
+gremlin> g.addV().property('name','stephen')
 ==>v[0]
 gremlin> g.V().elementMap()
 ==>[id:0,label:vertex,name:stephen]
-gremlin> g.V().property('name',null)
+gremlin> g.V().has('vertex','name','stephen').property('name',null)
 ==>v[0]
 gremlin> g.V().elementMap()
 ==>[id:0,label:vertex]
+...
+gremlin> g.getGraph().features().vertex().supportsNullPropertyValues()
+==>true
+gremlin> g.addV().property('name','stephen')
+==>v[2]
+gremlin> g.V().has('vertex','name','stephen').property('name',null)
+==>v[2]
+gremlin> g.V().elementMap()
+==>[id:2,label:vertex,name:null]
 ----
 
 The above examples point out the default operations of TinkerGraph, but it can be configured to actually accept the
@@ -413,20 +433,85 @@
 
 [source,text]
 ----
-gremlin> g.V(0L).as('a').addE('knows').to('a').property(id,null).property('weight',null)
-==>e[1][0-knows->0]
+gremlin> g.getGraph().features().vertex().supportsNullPropertyValues()
+==>false
+gremlin> g.addV().property('name','stephen')
+==>v[0]
+gremlin> g.V().has('vertex','name','stephen').as('a').addE('knows').to('a').property(id,null).property('weight',null)
+==>e[2][0-knows->0]
 gremlin> g.E().elementMap()
-==>[id:1,label:knows,IN:[id:0,label:vertex],OUT:[id:0,label:vertex]]
-gremlin> g.V(0L).as('a').addE('knows').to('a').property('weight',0.5)
-==>e[3][0-knows->0]
+==>[id:2,label:knows,IN:[id:0,label:vertex],OUT:[id:0,label:vertex]]
+gremlin> g.E().property('weight',0.5)
+==>e[2][0-knows->0]
 gremlin> g.E().elementMap()
-==>[id:3,label:knows,IN:[id:0,label:vertex],OUT:[id:0,label:vertex],weight:0.5]
+==>[id:2,label:knows,IN:[id:0,label:vertex],OUT:[id:0,label:vertex],weight:0.5]
 gremlin> g.E().property('weight',null)
-==>e[3][0-knows->0]
+==>e[2][0-knows->0]
 gremlin> g.E().elementMap()
-==>[id:3,label:knows,IN:[id:0,label:vertex],OUT:[id:0,label:vertex]]
+==>[id:2,label:knows,IN:[id:0,label:vertex],OUT:[id:0,label:vertex]]
+...
+gremlin> g.getGraph().features().vertex().supportsNullPropertyValues()
+==>true
+gremlin> g.addV().property('name','stephen')
+==>v[8]
+gremlin> g.V().has('vertex','name','stephen').as('a').addE('knows').to('a').property(id,null).property('weight',null)
+==>e[10][8-knows->8]
+gremlin> g.E().elementMap()
+==>[id:10,label:knows,IN:[id:8,label:vertex],OUT:[id:8,label:vertex],weight:null]
+gremlin> g.E().property('weight',0.5)
+==>e[10][8-knows->8]
+gremlin> g.E().elementMap()
+==>[id:10,label:knows,IN:[id:8,label:vertex],OUT:[id:8,label:vertex],weight:0.5]
+gremlin> g.E().property('weight',null)
+==>e[10][8-knows->8]
+gremlin> g.E().elementMap()
+==>[id:10,label:knows,IN:[id:8,label:vertex],OUT:[id:8,label:vertex],weight:null]
 ----
 
+Graphs that support multi/meta-properties have some issues to consider as well as demonstrated with TinkerGraph:
+
+[source,text]
+----
+gremlin> g.getGraph().features().vertex().supportsNullPropertyValues()
+==>false
+gremlin> g.addV().property(list,'foo',"x").property(list,"foo", null).property(list,'foo','bar')
+==>v[0]
+gremlin> g.V().elementMap()
+==>[id:0,label:vertex,foo:bar]
+gremlin> g.V().valueMap()
+==>[foo:[x,bar]]
+gremlin> g.V().property('foo',null)
+==>v[0]
+gremlin> g.V().valueMap(true)
+==>[id:0,label:vertex]
+...
+gremlin> g.addV().property(list,'foo','bar','x',1,'y',null)
+==>v[0]
+gremlin> g.V().properties('foo').valueMap(true)
+==>[id:1,key:foo,value:bar,x:1]
+gremlin> g.V().properties('foo').property('x',null)
+==>vp[foo->bar]
+gremlin> g.V().properties('foo').valueMap(true)
+==>[id:1,key:foo,value:bar]
+...
+gremlin> g.getGraph().features().vertex().supportsNullPropertyValues()
+==>false
+gremlin> g.addV().property(list,'foo',"x").property(list,"foo", null).property(list,'foo','bar')
+==>v[11]
+gremlin> g.V().elementMap()
+==>[id:11,label:vertex,foo:bar]
+gremlin> g.V().valueMap()
+==>[foo:[x,null,bar]]
+...
+gremlin> g.addV().property(list,'foo','bar','x',1,'y',null)
+==>v[0]
+gremlin> g.V().properties('foo').valueMap(true)
+==>[id:1,key:foo,value:bar,x:1,y:null]
+gremlin> g.V().properties('foo').property('x',null)
+==>vp[foo->bar]
+gremlin> g.V().properties('foo').valueMap(true)
+==>[id:1,key:foo,value:bar,x:null,y:null]
+----
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-2235[TINKERPOP-2235],
 link:https://issues.apache.org/jira/browse/TINKERPOP-2099[TINKERPOP-2099]
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java
index a8f352a..ef20e52 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.java
@@ -2191,8 +2191,7 @@
      * {@link VertexProperty.Cardinality} is defaulted to {@code null} which  means that the default cardinality for
      * the {@link Graph} will be used.
      * <p/>
-     * This method is effectively calls
-     * {@link #property(org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality, Object, Object, Object...)}
+     * This method is effectively calls {@link #property(VertexProperty.Cardinality, Object, Object, Object...)}
      * as {@code property(null, key, value, keyValues}.
      *
      * @param key       the key for the property
@@ -2204,8 +2203,8 @@
      */
     public default GraphTraversal<S, E> property(final Object key, final Object value, final Object... keyValues) {
         return key instanceof VertexProperty.Cardinality ?
-                this.property((VertexProperty.Cardinality) key, value, keyValues[0],
-                        keyValues.length > 1 ?
+                this.property((VertexProperty.Cardinality) key, value, null == keyValues ? null : keyValues[0],
+                        keyValues != null && keyValues.length > 1 ?
                                 Arrays.copyOfRange(keyValues, 1, keyValues.length) :
                                 new Object[]{}) :
                 this.property(null, key, value, keyValues);
diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/structure/VertexPropertyTest.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/structure/VertexPropertyTest.java
index 04b431d..c8b3c66 100644
--- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/structure/VertexPropertyTest.java
+++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/structure/VertexPropertyTest.java
@@ -111,7 +111,8 @@
         @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_META_PROPERTIES)
         @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_MULTI_PROPERTIES)
         @FeatureRequirement(featureClass = Graph.Features.VertexPropertyFeatures.class, feature = Graph.Features.VertexPropertyFeatures.FEATURE_INTEGER_VALUES)
-        public void shouldHandleListVertexProperties() {
+        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_NULL_PROPERTY_VALUES, supported = false)
+        public void shouldHandleListVertexPropertiesWithoutNullPropertyValues() {
             final Vertex v = graph.addVertex("name", "marko", "age", 34);
             tryCommit(graph, g -> {
                 assertEquals("marko", v.property("name").value());
@@ -169,6 +170,95 @@
 
                 assertVertexEdgeCounts(graph, 1, 0);
             });
+
+            // null property values are not supported so the value should not be added as set or list cardinality,
+            // but single will remove it
+            assertEquals(VertexProperty.empty(), v.property(VertexProperty.Cardinality.list, "name", null));
+            tryCommit(graph, g -> {
+                assertEquals(3, IteratorUtils.count(v.properties("name")));
+                assertEquals(4, IteratorUtils.count(v.properties()));
+                assertVertexEdgeCounts(graph, 1, 0);
+            });
+            assertEquals(VertexProperty.empty(), v.property(VertexProperty.Cardinality.single, "name", null));
+            tryCommit(graph, g -> {
+                assertEquals(0, IteratorUtils.count(v.properties("name")));
+                assertEquals(1, IteratorUtils.count(v.properties()));
+                assertVertexEdgeCounts(graph, 1, 0);
+            });
+        }
+
+        @Test
+        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
+        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_META_PROPERTIES)
+        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_MULTI_PROPERTIES)
+        @FeatureRequirement(featureClass = Graph.Features.VertexPropertyFeatures.class, feature = Graph.Features.VertexPropertyFeatures.FEATURE_INTEGER_VALUES)
+        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_NULL_PROPERTY_VALUES)
+        public void shouldHandleListVertexPropertiesWithNullPropertyValues() {
+            final Vertex v = graph.addVertex("name", "marko", "age", 34);
+            tryCommit(graph, g -> {
+                assertEquals("marko", v.property("name").value());
+                assertEquals("marko", v.value("name"));
+                assertEquals(34, v.property("age").value());
+                assertEquals(34, v.<Integer>value("age").intValue());
+                assertEquals(1, IteratorUtils.count(v.properties("name")));
+                assertEquals(2, IteratorUtils.count(v.properties()));
+                assertVertexEdgeCounts(graph, 1, 0);
+            });
+
+            final VertexProperty<String> property = v.property(VertexProperty.Cardinality.list, "name", "marko a. rodriguez");
+            tryCommit(graph, g -> assertEquals(v, property.element()));
+
+            try {
+                v.property("name");
+                fail("This should throw a: " + Vertex.Exceptions.multiplePropertiesExistForProvidedKey("name"));
+            } catch (final Exception e) {
+                validateException(Vertex.Exceptions.multiplePropertiesExistForProvidedKey("name"), e);
+            }
+
+            assertTrue(IteratorUtils.list(v.values("name")).contains("marko"));
+            assertTrue(IteratorUtils.list(v.values("name")).contains("marko a. rodriguez"));
+            assertEquals(3, IteratorUtils.count(v.properties()));
+            assertEquals(2, IteratorUtils.count(v.properties("name")));
+            assertVertexEdgeCounts(graph, 1, 0);
+
+            assertEquals(v, v.property(VertexProperty.Cardinality.list, "name", "mrodriguez").element());
+            tryCommit(graph, g -> {
+                assertEquals(3, IteratorUtils.count(v.properties("name")));
+                assertEquals(4, IteratorUtils.count(v.properties()));
+                assertVertexEdgeCounts(graph, 1, 0);
+            });
+
+            v.<String>properties("name").forEachRemaining(meta -> {
+                meta.property("counter", meta.value().length());
+            });
+
+            tryCommit(graph, g -> {
+                v.properties().forEachRemaining(meta -> {
+                    assertEquals(meta.key(), meta.label());
+                    assertTrue(meta.isPresent());
+                    assertEquals(v, meta.element());
+                    if (meta.key().equals("age")) {
+                        assertEquals(meta.value(), 34);
+                        assertEquals(0, IteratorUtils.count(meta.properties()));
+                    }
+                    if (meta.key().equals("name")) {
+                        assertEquals(((String) meta.value()).length(), meta.<Integer>value("counter").intValue());
+                        assertEquals(1, IteratorUtils.count(meta.properties()));
+                        assertEquals(1, meta.keys().size());
+                        assertTrue(meta.keys().contains("counter"));
+                    }
+                });
+
+                assertVertexEdgeCounts(graph, 1, 0);
+            });
+
+            // null property values are supported so the value should be added
+            assertEquals(v, v.property(VertexProperty.Cardinality.list, "name", null).element());
+            tryCommit(graph, g -> {
+                assertEquals(4, IteratorUtils.count(v.properties("name")));
+                assertEquals(5, IteratorUtils.count(v.properties()));
+                assertVertexEdgeCounts(graph, 1, 0);
+            });
         }
 
         @Test
diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java
index 52518d6..1f055fb 100644
--- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java
+++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertex.java
@@ -89,8 +89,12 @@
         ElementHelper.validateProperty(key, value);
         final Optional<Object> optionalId = ElementHelper.getIdValue(keyValues);
 
+        // if we don't allow null property values and the value is null then the key can be removed but only if the
+        // cardinality is single. if it is list/set then we can just ignore the null.
         if (!allowNullPropertyValues && null == value) {
-            properties(key).forEachRemaining(VertexProperty::remove);
+            final VertexProperty.Cardinality card = null == cardinality ? graph.features().vertex().getCardinality(key) : cardinality;
+            if (VertexProperty.Cardinality.single == card)
+                properties(key).forEachRemaining(VertexProperty::remove);
             return VertexProperty.empty();
         }
 
diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexProperty.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexProperty.java
index ed8acec..f2635df 100644
--- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexProperty.java
+++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerVertexProperty.java
@@ -143,7 +143,8 @@
             }
             final AtomicBoolean delete = new AtomicBoolean(true);
             this.vertex.properties(this.key).forEachRemaining(property -> {
-                if (property.value().equals(this.value))
+                final Object currentPropertyValue = property.value();
+                if ((currentPropertyValue != null && currentPropertyValue.equals(this.value) || null == currentPropertyValue && null == this.value))
                     delete.set(false);
             });
             if (delete.get()) TinkerHelper.removeIndex(this.vertex, this.key, this.value);