Merge branch 'feature/SLING-8066'
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockNode.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockNode.java
index db7bdc0..27806c8 100644
--- a/src/main/java/org/apache/sling/testing/mock/jcr/MockNode.java
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockNode.java
@@ -195,8 +195,7 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(value);
-        getMockedSession().addItem(itemData);
-        this.itemData.setIsChanged(true);
+        addItemOrRemoveIfValueNull(itemData, value);
         return property;
     }
 
@@ -205,8 +204,7 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(values);
-        getMockedSession().addItem(itemData);
-        this.itemData.setIsChanged(true);
+        addItemOrRemoveIfValueNull(itemData, values);
         return property;
     }
 
@@ -215,8 +213,7 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(values);
-        getMockedSession().addItem(itemData);
-        this.itemData.setIsChanged(true);
+        addItemOrRemoveIfValueNull(itemData, values);
         return property;
     }
 
@@ -225,8 +222,7 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(value);
-        getMockedSession().addItem(itemData);
-        this.itemData.setIsChanged(true);
+        addItemOrRemoveIfValueNull(itemData, value);
         return property;
     }
 
@@ -236,8 +232,7 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(value);
-        getMockedSession().addItem(itemData);
-        this.itemData.setIsChanged(true);
+        addItemOrRemoveIfValueNull(itemData, value);
         return property;
     }
 
@@ -246,8 +241,7 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(value);
-        getMockedSession().addItem(itemData);
-        this.itemData.setIsChanged(true);
+        addItem(itemData);
         return property;
     }
 
@@ -256,8 +250,7 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(value);
-        getMockedSession().addItem(itemData);
-        this.itemData.setIsChanged(true);
+        addItem(itemData);
         return property;
     }
 
@@ -266,8 +259,7 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(value);
-        getMockedSession().addItem(itemData);
-        this.itemData.setIsChanged(true);
+        addItem(itemData);
         return property;
     }
 
@@ -276,8 +268,7 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(value);
-        getMockedSession().addItem(itemData);
-        this.itemData.setIsChanged(true);
+        addItemOrRemoveIfValueNull(itemData, value);
         return property;
     }
 
@@ -286,8 +277,7 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(value);
-        getMockedSession().addItem(itemData);
-        this.itemData.setIsChanged(true);
+        addItemOrRemoveIfValueNull(itemData, value);
         return property;
     }
 
@@ -296,8 +286,7 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(value);
-        getMockedSession().addItem(itemData);
-        this.itemData.setIsChanged(true);
+        addItemOrRemoveIfValueNull(itemData, value);
         return property;
     }
 
@@ -306,9 +295,34 @@
         ItemData itemData = ItemData.newProperty(getPath() + "/" + name);
         Property property = new MockProperty(itemData, getSession());
         property.setValue(value);
+        addItemOrRemoveIfValueNull(itemData, value);
+        return property;
+    }
+    
+    /**
+     * Adds or overwrites an item and marks the item value as changed.
+     * @param itemData Item data
+     * @throws RepositoryException
+     */
+    private void addItem(ItemData itemData) throws RepositoryException {
         getMockedSession().addItem(itemData);
         this.itemData.setIsChanged(true);
-        return property;
+    }
+
+    /**
+     * Adds or overwrites an item and marks the item value as changed.
+     * If the given value is null, the item is removed instead.
+     * @param itemData Item data
+     * @throws RepositoryException
+     */
+    private void addItemOrRemoveIfValueNull(ItemData itemData, Object value) throws RepositoryException {
+        if (value == null) {
+            getMockedSession().removeItem(itemData.getPath());
+        }
+        else {
+            getMockedSession().addItem(itemData);
+        }
+        this.itemData.setIsChanged(true);
     }
 
     @Override
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockProperty.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockProperty.java
index 291a165..bb15aef 100644
--- a/src/main/java/org/apache/sling/testing/mock/jcr/MockProperty.java
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockProperty.java
@@ -80,12 +80,18 @@
 
     @Override
     public void setValue(final Value newValue) throws RepositoryException {
+        if (removePropertyIfValueNull(newValue)) {
+            return;
+        }
         this.itemData.setValues(new Value[] { newValue });
         this.itemData.setMultiple(false);
     }
 
     @Override
     public void setValue(final Value[] newValues) throws RepositoryException {
+        if (removePropertyIfValueNull(newValues)) {
+            return;
+        }
         Value[] values = new Value[newValues.length];
         for (int i = 0; i < newValues.length; i++) {
             values[i] = newValues[i];
@@ -96,12 +102,18 @@
 
     @Override
     public void setValue(final String newValue) throws RepositoryException {
+        if (removePropertyIfValueNull(newValue)) {
+            return;
+        }
         this.itemData.setValues(new Value[] { getSession().getValueFactory().createValue(newValue) });
         this.itemData.setMultiple(false);
     }
 
     @Override
     public void setValue(final String[] newValues) throws RepositoryException {
+        if (removePropertyIfValueNull(newValues)) {
+            return;
+        }
         Value[] values = new Value[newValues.length];
         for (int i = 0; i < newValues.length; i++) {
             values[i] = getSession().getValueFactory().createValue(newValues[i]);
@@ -112,6 +124,9 @@
 
     @Override
     public void setValue(final InputStream newValue) throws RepositoryException {
+        if (removePropertyIfValueNull(newValue)) {
+            return;
+        }
         this.itemData.setValues(new Value[] { new BinaryValue(newValue) });
         this.itemData.setMultiple(false);
     }
@@ -130,6 +145,9 @@
 
     @Override
     public void setValue(final Calendar newValue) throws RepositoryException {
+        if (removePropertyIfValueNull(newValue)) {
+            return;
+        }
         this.itemData.setValues(new Value[] { getSession().getValueFactory().createValue(newValue) });
         this.itemData.setMultiple(false);
     }
@@ -142,21 +160,44 @@
 
     @Override
     public void setValue(final Node newValue) throws RepositoryException {
+        if (removePropertyIfValueNull(newValue)) {
+            return;
+        }
         this.itemData.setValues(new Value[] { getSession().getValueFactory().createValue(newValue) });
         this.itemData.setMultiple(false);
     }
 
     @Override
     public void setValue(final Binary newValue) throws RepositoryException {
+        if (removePropertyIfValueNull(newValue)) {
+            return;
+        }
         this.itemData.setValues(new Value[] { new BinaryValue(newValue) });
         this.itemData.setMultiple(false);
     }
 
     @Override
     public void setValue(final BigDecimal newValue) throws RepositoryException {
+        if (removePropertyIfValueNull(newValue)) {
+            return;
+        }
         this.itemData.setValues(new Value[] { getSession().getValueFactory().createValue(newValue) });
         this.itemData.setMultiple(false);
     }
+    
+    /**
+     * Removes the current property (itself) if the given value is null.
+     * @param value Value to check
+     * @return true if property was removed
+     * @throws RepositoryException
+     */
+    private boolean removePropertyIfValueNull(Object value) throws RepositoryException {
+        if (value == null) {
+            remove();
+            return true;
+        }
+        return false;
+    }
 
     @Override
     public boolean getBoolean() throws RepositoryException {
@@ -206,7 +247,7 @@
         }
         else {
             return PropertyType.UNDEFINED;
-        }    
+        }
     }
 
     @Override
@@ -253,7 +294,7 @@
         }
         return false;
     }
-    
+
     // --- unsupported operations ---
     @Override
     public Node getNode() throws RepositoryException {
@@ -286,7 +327,7 @@
         public boolean isProtected() {
             return false;
         }
-        
+
         @Override
         public boolean isFullTextSearchable() {
             return false;
@@ -296,7 +337,7 @@
         public boolean isQueryOrderable() {
             return false;
         }
-        
+
         // --- unsupported operations ---
         @Override
         public Value[] getDefaultValues() {
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockPropertyTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockPropertyTest.java
index 949dbb3..640ff8c 100644
--- a/src/test/java/org/apache/sling/testing/mock/jcr/MockPropertyTest.java
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockPropertyTest.java
@@ -25,6 +25,7 @@
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.math.BigDecimal;
 import java.util.Calendar;
 
@@ -87,6 +88,22 @@
     }
 
     @Test
+    public void testStringSetNullViaNode() throws RepositoryException {
+        this.node1.setProperty("prop1", "value1");
+
+        this.node1.setProperty("prop1", (String)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+    
+    @Test
+    public void testStringSetNullViaProp() throws RepositoryException {
+        this.node1.setProperty("prop1", "value1");
+
+        this.node1.getProperty("prop1").setValue((String)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+    
+    @Test
     public void testStringArray() throws RepositoryException {
         String[] value1 = new String[] { "aaa", "bbb" };
         this.node1.setProperty("prop1", value1);
@@ -110,6 +127,24 @@
     }
 
     @Test
+    public void testStringArraySetNullViaNode() throws RepositoryException {
+        String[] value1 = new String[] { "aaa", "bbb" };
+        this.node1.setProperty("prop1", value1);
+
+        this.node1.setProperty("prop1", (String[])null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @Test
+    public void testStringArraySetNullViaProp() throws RepositoryException {
+        String[] value1 = new String[] { "aaa", "bbb" };
+        this.node1.setProperty("prop1", value1);
+
+        this.node1.getProperty("prop1").setValue((String[])null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @Test
     public void testBoolean() throws RepositoryException {
         this.node1.setProperty("prop1", true);
         Property prop1 = this.node1.getProperty("prop1");
@@ -158,6 +193,22 @@
     }
 
     @Test
+    public void testBigDecimalSetNullViaNode() throws RepositoryException {
+        this.node1.setProperty("prop1", new BigDecimal("1.5"));
+
+        this.node1.setProperty("prop1", (BigDecimal)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @Test
+    public void testBigDecimalSetNullViaProp() throws RepositoryException {
+        this.node1.setProperty("prop1", new BigDecimal("1.5"));
+
+        this.node1.getProperty("prop1").setValue((BigDecimal)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @Test
     public void testCalendar() throws RepositoryException {
         Calendar value1 = Calendar.getInstance();
 
@@ -175,6 +226,24 @@
     }
 
     @Test
+    public void testCalendarSetNullViaNode() throws RepositoryException {
+        Calendar value1 = Calendar.getInstance();
+        this.node1.setProperty("prop1", value1);
+
+        this.node1.setProperty("prop1", (Calendar)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @Test
+    public void testCalendarSetNullViaProp() throws RepositoryException {
+        Calendar value1 = Calendar.getInstance();
+        this.node1.setProperty("prop1", value1);
+
+        this.node1.getProperty("prop1").setValue((Calendar)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @Test
     public void testBinary() throws RepositoryException, IOException {
         byte[] value1 = new byte[] { 0x01, 0x01, 0x03 };
 
@@ -190,6 +259,24 @@
         assertArrayEquals(value2, IOUtils.toByteArray(prop1.getValue().getBinary().getStream()));
     }
 
+    @Test
+    public void testBinarySetNullViaNode() throws RepositoryException {
+        byte[] value1 = new byte[] { 0x01, 0x01, 0x03 };
+        this.node1.setProperty("prop1", new BinaryValue(value1).getBinary());
+
+        this.node1.setProperty("prop1", (BinaryValue)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @Test
+    public void testBinarySetNullViaProp() throws RepositoryException {
+        byte[] value1 = new byte[] { 0x01, 0x01, 0x03 };
+        this.node1.setProperty("prop1", new BinaryValue(value1).getBinary());
+
+        this.node1.getProperty("prop1").setValue((BinaryValue)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
     @SuppressWarnings("deprecation")
     @Test
     public void testInputStream() throws RepositoryException, IOException {
@@ -205,6 +292,26 @@
         assertArrayEquals(value2, IOUtils.toByteArray(prop1.getValue().getStream()));
     }
 
+    @SuppressWarnings("deprecation")
+    @Test
+    public void testInputStreamSetNullViaNode() throws RepositoryException {
+        byte[] value1 = new byte[] { 0x01, 0x01, 0x03 };
+        this.node1.setProperty("prop1", new ByteArrayInputStream(value1));
+
+        this.node1.setProperty("prop1", (InputStream)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void testInputStreamSetNullViaProp() throws RepositoryException {
+        byte[] value1 = new byte[] { 0x01, 0x01, 0x03 };
+        this.node1.setProperty("prop1", new ByteArrayInputStream(value1));
+
+        this.node1.getProperty("prop1").setValue((InputStream)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
     @Test
     public void testValue() throws RepositoryException {
         this.node1.setProperty("prop1", this.session.getValueFactory().createValue("value1"));
@@ -222,6 +329,22 @@
     }
 
     @Test
+    public void testValueSetNullViaNode() throws RepositoryException {
+        this.node1.setProperty("prop1", this.session.getValueFactory().createValue("value1"));
+
+        this.node1.setProperty("prop1", (Value)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @Test
+    public void testValueSetNullViaProp() throws RepositoryException {
+        this.node1.setProperty("prop1", this.session.getValueFactory().createValue("value1"));
+
+        this.node1.getProperty("prop1").setValue((Value)null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @Test
     public void testValueArray() throws RepositoryException {
         Value[] value1 = new Value[] { this.session.getValueFactory().createValue("aaa"),
                 this.session.getValueFactory().createValue("bbb") };
@@ -246,6 +369,26 @@
     }
 
     @Test
+    public void testValueArraySetNullViaNode() throws RepositoryException {
+        Value[] value1 = new Value[] { this.session.getValueFactory().createValue("aaa"),
+                this.session.getValueFactory().createValue("bbb") };
+        this.node1.setProperty("prop1", value1);
+
+        this.node1.setProperty("prop1", (Value[])null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @Test
+    public void testValueArraySetNullViaProp() throws RepositoryException {
+        Value[] value1 = new Value[] { this.session.getValueFactory().createValue("aaa"),
+                this.session.getValueFactory().createValue("bbb") };
+        this.node1.setProperty("prop1", value1);
+
+        this.node1.getProperty("prop1").setValue((Value[])null);
+        assertFalse(this.node1.hasProperty("prop1"));        
+    }
+
+    @Test
     public void testEmptyArrayGetType() throws RepositoryException {
         this.node1.setProperty("prop1", new Value[] {});
         Property prop1 = this.node1.getProperty("prop1");