You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by se...@apache.org on 2021/01/24 20:28:01 UTC

[directory-studio] 01/01: DIRSTUDIO-744: Fix entry modify requests

This is an automated email from the ASF dual-hosted git repository.

seelmann pushed a commit to branch DIRSTUDIO-744-fix-entry-modify-request
in repository https://gitbox.apache.org/repos/asf/directory-studio.git

commit 294c29d4c071276a9612eedaabfbba51a1111908
Author: Stefan Seelmann <ma...@stefan-seelmann.de>
AuthorDate: Sun Jan 24 21:26:35 2021 +0100

    DIRSTUDIO-744: Fix entry modify requests
    
    * When deleting all remaining values of an attribute the deleted values are now included in the modify request.
    * Remove "smart" behaviour whicht used replace operation instead of delete+add when modifying a value.
    * Both can cause unwanted side effect if the client doesn't see all values.
---
 .../docbook/2.40_tools_newconnection_wizard.xml    |  22 +-
 .../docbook/2.80_tools_connection_properties.xml   |  18 +-
 .../common/widgets/connection/messages.properties  |   6 +-
 .../studio/ldapbrowser/core/utils/Utils.java       |  15 +-
 tests/test.integration.core/pom-first.xml          |   1 +
 .../test/integration/core/ComputeDiffTest.java     | 309 +++++++++++++++++++++
 .../test/integration/ui/EntryEditorTest.java       |  70 ++++-
 .../test/integration/ui/ErrorHandlingTest.java     |   3 +-
 .../studio/test/integration/ui/OpenLdapTest.java   | 133 +++++++++
 .../studio/test/integration/ui/OpenLdapTest.ldif   |   1 -
 10 files changed, 536 insertions(+), 42 deletions(-)

diff --git a/helps/ldapbrowser.help/src/main/docbook/2.40_tools_newconnection_wizard.xml b/helps/ldapbrowser.help/src/main/docbook/2.40_tools_newconnection_wizard.xml
index 19f1a6d..c5312b6 100644
--- a/helps/ldapbrowser.help/src/main/docbook/2.40_tools_newconnection_wizard.xml
+++ b/helps/ldapbrowser.help/src/main/docbook/2.40_tools_newconnection_wizard.xml
@@ -574,15 +574,15 @@
 								Specify the modify mode for attributes with an equality matching rule.
 								Description of options:
 								<itemizedlist spacing="normal" mark="bullet">
-									<listitem>Optimized Modify Operations: uses add/delete by default, 
-									uses replace if operation count is less</listitem>
+									<listitem>Default: uses add/delete by default, 
+									uses replace for X-ORDERED attributes</listitem>
 									<listitem>Always REPLACE: always uses replace operations to perform 
 									entry modifications</listitem>
 									<listitem>Always ADD/DELETE: always uses add and/or delete operations 
 									to perform entry modifications</listitem>
 								</itemizedlist>
 							</entry>
-							<entry>Optimized Modify Operations</entry>
+							<entry>Default</entry>
 						</row>
 						<row>
 							<entry>Modify Mode (no equality matching rule)</entry>
@@ -590,8 +590,8 @@
 								Specify the modify mode for attributes with *no* equality matching rule.
 								Description of options:
 								<itemizedlist spacing="normal" mark="bullet">
-									<listitem>Optimized Modify Operations: uses add/delete by default, 
-									uses replace if operation count is less</listitem>
+									<listitem>Default: uses add/delete by default, 
+									uses replace for X-ORDERED attributes</listitem>
 									<listitem>Always REPLACE: always uses replace operations to perform 
 									entry modifications</listitem>
 									<listitem>Always ADD/DELETE: always uses add and/or delete operations 
@@ -599,18 +599,18 @@
 								</itemizedlist>
 								Recommended values for various LDAP servers:
 								<itemizedlist spacing="normal" mark="bullet">
-									<listitem>ApacheDS: Optimized Modify Operations or REPLACE</listitem>
+									<listitem>ApacheDS: Default or REPLACE</listitem>
 									<listitem>OpenLDAP: REPLACE</listitem>
-									<listitem>OpenDS / SunDSEE: Optimized Modify Operations or REPLACE</listitem>
-									<listitem>FedoraDS / 389DS: Optimized Modify Operations 
+									<listitem>OpenDS / SunDSEE: Default or REPLACE</listitem>
+									<listitem>FedoraDS / 389DS: Default 
 									(missing equality matching rules for many standard attribute types)</listitem>
-									<listitem>Active Directory: Optimized Modify Operations 
+									<listitem>Active Directory: Default 
 									(exposes no equality matching rules at all)</listitem>
-									<listitem>eDirectory: Optimized Modify Operations 
+									<listitem>eDirectory: Default 
 									(exposes no equality matching rules at all)</listitem>
 								</itemizedlist>
 							</entry>
-							<entry>Optimized Modify Operations</entry>
+							<entry>Default</entry>
 						</row>
 						<row>
 							<entry>Modify Order</entry>
diff --git a/helps/ldapbrowser.help/src/main/docbook/2.80_tools_connection_properties.xml b/helps/ldapbrowser.help/src/main/docbook/2.80_tools_connection_properties.xml
index c080548..68ed5f4 100644
--- a/helps/ldapbrowser.help/src/main/docbook/2.80_tools_connection_properties.xml
+++ b/helps/ldapbrowser.help/src/main/docbook/2.80_tools_connection_properties.xml
@@ -437,8 +437,8 @@
 							Specify the modify mode for attributes with an equality matching rule.
 							Description of options:
 							<itemizedlist spacing="normal" mark="bullet">
-								<listitem>Optimized Modify Operations: uses add/delete by default, 
-								uses replace if operation count is less</listitem>
+								<listitem>Default: uses add/delete by default, 
+								uses replace for X-ORDERED attributes</listitem>
 								<listitem>Always REPLACE: always uses replace operations to perform 
 								entry modifications</listitem>
 								<listitem>Always ADD/DELETE: always uses add and/or delete operations 
@@ -452,8 +452,8 @@
 							Specify the modify mode for attributes with *no* equality matching rule.
 							Description of options:
 							<itemizedlist spacing="normal" mark="bullet">
-								<listitem>Optimized Modify Operations: uses add/delete by default, 
-								uses replace if operation count is less</listitem>
+								<listitem>Default: uses add/delete by default, 
+								uses replace for X-ORDERED attributes</listitem>
 								<listitem>Always REPLACE: always uses replace operations to perform 
 								entry modifications</listitem>
 								<listitem>Always ADD/DELETE: always uses add and/or delete operations 
@@ -461,14 +461,14 @@
 							</itemizedlist>
 							Recommended values for various LDAP servers:
 							<itemizedlist spacing="normal" mark="bullet">
-								<listitem>ApacheDS: Optimized Modify Operations or REPLACE</listitem>
+								<listitem>ApacheDS: Default or REPLACE</listitem>
 								<listitem>OpenLDAP: REPLACE</listitem>
-								<listitem>OpenDS / SunDSEE: Optimized Modify Operations or REPLACE</listitem>
-								<listitem>FedoraDS / 389DS: Optimized Modify Operations 
+								<listitem>OpenDS / SunDSEE: Default or REPLACE</listitem>
+								<listitem>FedoraDS / 389DS: Default 
 								(missing equality matching rules for many standard attribute types)</listitem>
-								<listitem>Active Directory: Optimized Modify Operations 
+								<listitem>Active Directory: Default 
 								(exposes no equality matching rules at all)</listitem>
-								<listitem>eDirectory: Optimized Modify Operations 
+								<listitem>eDirectory: Default 
 								(exposes no equality matching rules at all)</listitem>
 							</itemizedlist>
 						</entry>
diff --git a/plugins/ldapbrowser.common/src/main/java/org/apache/directory/studio/ldapbrowser/common/widgets/connection/messages.properties b/plugins/ldapbrowser.common/src/main/java/org/apache/directory/studio/ldapbrowser/common/widgets/connection/messages.properties
index fd9548f..b1d9963 100644
--- a/plugins/ldapbrowser.common/src/main/java/org/apache/directory/studio/ldapbrowser/common/widgets/connection/messages.properties
+++ b/plugins/ldapbrowser.common/src/main/java/org/apache/directory/studio/ldapbrowser/common/widgets/connection/messages.properties
@@ -38,11 +38,11 @@ BrowserParameterPage.ManageDsaItWhileBrowsingTooltip=If enabled the ManageDsaIT
 EditorParameterPage.ModifyGroup=Entry Modifcation
 EditorParameterPage.ModifyMode=Modify Mode:
 EditorParameterPage.ModifyModeAddDel=Always use ADD and/or DELETE
-EditorParameterPage.ModifyModeDefault=Optimized Modify Operations
+EditorParameterPage.ModifyModeDefault=Default
 EditorParameterPage.ModifyModeNoEMR=Modify Mode (no equality matching rule):
-EditorParameterPage.ModifyModeNoEMRTooltip=Specify the modify mode for attributes with *no* equality matching rule.\n\nDescription of options:\n* Optimized Modify Operations: uses add/delete by default, uses replace if operation count is less\n* Always use REPLACE: always uses replace operations to perform entry modifications\n* Always use ADD and/or DELETE: always uses add and/or delete operations to perform entry modifications\n\nRecommended values for various LDAP servers:\n* ApacheDS [...]
+EditorParameterPage.ModifyModeNoEMRTooltip=Specify the modify mode for attributes with *no* equality matching rule.\n\nDescription of options:\n* Default: uses add/delete by default, uses replace for X-ORDERED attributes\n* Always use REPLACE: always uses replace operations to perform entry modifications\n* Always use ADD and/or DELETE: always uses add and/or delete operations to perform entry modifications\n\nRecommended values for various LDAP servers:\n* ApacheDS: Default or REPLACE\n [...]
 EditorParameterPage.ModifyModeReplace=Always use REPLACE
-EditorParameterPage.ModifyModeTooltip=Specify the modify mode for attributes with an equality matching rule.\n\nDescription of options:\n* Optimized Modify Operations: uses add/delete by default, uses replace if operation count is less\n* Always REPLACE: always uses replace operations to perform entry modifications\n* Always ADD/DELETE: always uses add and/or delete operations to perform entry modifications\n\nRecommended value: Optimized Modify Operations
+EditorParameterPage.ModifyModeTooltip=Specify the modify mode for attributes with an equality matching rule.\n\nDescription of options:\n* Default: uses add/delete by default, uses replace for X-ORDERED attributes\n* Always REPLACE: always uses replace operations to perform entry modifications\n* Always ADD/DELETE: always uses add and/or delete operations to perform entry modifications\n\nRecommended value: Default
 EditorParameterPage.ModifyOrder=Modify Order:
 EditorParameterPage.ModifyOrderAddFirst=ADD First
 EditorParameterPage.ModifyOrderDelFirst=DELETE First
diff --git a/plugins/ldapbrowser.core/src/main/java/org/apache/directory/studio/ldapbrowser/core/utils/Utils.java b/plugins/ldapbrowser.core/src/main/java/org/apache/directory/studio/ldapbrowser/core/utils/Utils.java
index af5b4fa..2cc7bad 100644
--- a/plugins/ldapbrowser.core/src/main/java/org/apache/directory/studio/ldapbrowser/core/utils/Utils.java
+++ b/plugins/ldapbrowser.core/src/main/java/org/apache/directory/studio/ldapbrowser/core/utils/Utils.java
@@ -30,6 +30,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -406,7 +407,7 @@ public class Utils
         ModifyOrder modifyAddDeleteOrder = oldEntry.getBrowserConnection().getModifyAddDeleteOrder();
 
         // get all attribute descriptions
-        Set<String> attributeDescriptions = new HashSet<>();
+        Set<String> attributeDescriptions = new LinkedHashSet<>();
         
         for ( IAttribute oldAttr : oldEntry.getAttributes() )
         {
@@ -487,6 +488,10 @@ public class Utils
                 {
                     // delete all
                     modSpec = LdifModSpec.createDelete( attributeDescription );
+                    for ( IValue value : oldAttribute.getValues() )
+                    {
+                        modSpec.addAttrVal( computeDiffCreateAttrValLine( value ) );
+                    }
                 }
                 
                 modSpec.finish( LdifModSpecSepLine.create() );
@@ -558,16 +563,12 @@ public class Utils
                     /*
                      *  we use add/del in the following cases:
                      *  - add/del is forced in the connection configuration
-                     *  - only values to add
-                     *  - only values to delete
-                     *  - the sum of adds and deletes is smaller or equal than the number of replaces
+                     *  - for attributes w/o X-ORDERED 'VALUES'
                      *  
                      *  we use replace in the following cases:
-                     *  - the number of replaces is smaller to the sum of adds and deletes
                      *  - for attributes with X-ORDERED 'VALUES'
                      */
-                    if ( isAddDelForced || ( toAdd.size() + toDel.size() <= newAttrValLines.size() && !isOrderedValue )
-                        || ( !toDel.isEmpty() && toAdd.isEmpty() ) || ( !toAdd.isEmpty() && toDel.isEmpty() ) )
+                    if ( isAddDelForced || !isOrderedValue )
                     {
                         // add/del del/add
                         LdifModSpec addModSpec = LdifModSpec.createAdd( attributeDescription );
diff --git a/tests/test.integration.core/pom-first.xml b/tests/test.integration.core/pom-first.xml
index 70a38f9..9a1a9ad 100644
--- a/tests/test.integration.core/pom-first.xml
+++ b/tests/test.integration.core/pom-first.xml
@@ -48,6 +48,7 @@
             <Bundle-Activator>org.apache.directory.studio.test.integration.core.Activator</Bundle-Activator>
             
             <Require-Bundle>org.junit;bundle-version="4.11.0",
+ org.hamcrest.library;bundle-version="1.3.0",
  org.apache.directory.server.apacheds-test-framework;bundle-version="${org.apache.directory.server.version}",
  org.apache.directory.server.annotations;bundle-version="${org.apache.directory.server.version}",
  org.apache.directory.server.xdbm.partition;bundle-version="${org.apache.directory.server.version}",
diff --git a/tests/test.integration.core/src/main/java/org/apache/directory/studio/test/integration/core/ComputeDiffTest.java b/tests/test.integration.core/src/main/java/org/apache/directory/studio/test/integration/core/ComputeDiffTest.java
new file mode 100644
index 0000000..1f7cc8e
--- /dev/null
+++ b/tests/test.integration.core/src/main/java/org/apache/directory/studio/test/integration/core/ComputeDiffTest.java
@@ -0,0 +1,309 @@
+/*
+ *  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.directory.studio.test.integration.core;
+
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertNull;
+
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.studio.connection.core.event.ConnectionEventRegistry;
+import org.apache.directory.studio.ldapbrowser.core.model.IBrowserConnection;
+import org.apache.directory.studio.ldapbrowser.core.model.IBrowserConnection.ModifyMode;
+import org.apache.directory.studio.ldapbrowser.core.model.IEntry;
+import org.apache.directory.studio.ldapbrowser.core.model.impl.Attribute;
+import org.apache.directory.studio.ldapbrowser.core.model.impl.DummyConnection;
+import org.apache.directory.studio.ldapbrowser.core.model.impl.DummyEntry;
+import org.apache.directory.studio.ldapbrowser.core.model.impl.Value;
+import org.apache.directory.studio.ldapbrowser.core.model.schema.Schema;
+import org.apache.directory.studio.ldapbrowser.core.utils.Utils;
+import org.apache.directory.studio.ldifparser.LdifParserConstants;
+import org.apache.directory.studio.ldifparser.model.LdifFile;
+import org.apache.directory.studio.ldifparser.model.container.LdifChangeModifyRecord;
+import org.junit.Before;
+import org.junit.Test;
+
+
+public class ComputeDiffTest
+{
+    private IBrowserConnection connection;
+
+    private IEntry oldEntry;
+    private IEntry newEntry;
+
+    static class TestConnection extends DummyConnection
+    {
+        private static final long serialVersionUID = 1L;
+
+        private ModifyMode modifyMode = ModifyMode.DEFAULT;
+        private ModifyMode modifyModeNoEMR = ModifyMode.DEFAULT;
+
+        public TestConnection()
+        {
+            super( Schema.DEFAULT_SCHEMA );
+        }
+
+
+        public ModifyMode getModifyMode()
+        {
+            return modifyMode;
+        }
+
+
+        public void setModifyMode( ModifyMode mode )
+        {
+            this.modifyMode = mode;
+        }
+
+
+        public ModifyMode getModifyModeNoEMR()
+        {
+            return modifyModeNoEMR;
+        }
+
+
+        public void setModifyModeNoEMR( ModifyMode mode )
+        {
+            this.modifyModeNoEMR = mode;
+        }
+
+    }
+
+    @Before
+    public void setup() throws Exception
+    {
+        ConnectionEventRegistry.suspendEventFiringInCurrentThread();
+        connection = new TestConnection();
+        oldEntry = new DummyEntry( new Dn( "cn=foo" ), connection );
+        newEntry = new DummyEntry( new Dn( "cn=foo" ), connection );
+    }
+
+
+    @Test
+    public void shouldReturnNullForEqualEntries()
+    {
+        // entries without attribute
+        assertNull( Utils.computeDiff( oldEntry, newEntry ) );
+        assertNull( Utils.computeDiff( oldEntry, oldEntry ) );
+        assertNull( Utils.computeDiff( newEntry, newEntry ) );
+
+        // entries with attributes
+        addAttribute( oldEntry, "cn", "1" );
+        addAttribute( oldEntry, "member", "cn=1", "cn=2", "cn=3" );
+        addAttribute( newEntry, "cn", "1" );
+        addAttribute( newEntry, "member", "cn=1", "cn=2", "cn=3" );
+        assertNull( Utils.computeDiff( oldEntry, newEntry ) );
+        assertNull( Utils.computeDiff( oldEntry, oldEntry ) );
+        assertNull( Utils.computeDiff( newEntry, newEntry ) );
+    }
+
+
+    @Test
+    public void shouldAddOneAttributeWithOneValue()
+    {
+        addAttribute( newEntry, "cn", "1" );
+
+        connection.setModifyMode( ModifyMode.DEFAULT );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ), "add:cn", "cn:1" );
+
+        connection.setModifyMode( ModifyMode.REPLACE );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ), "replace:cn", "cn:1" );
+    }
+
+
+    @Test
+    public void shouldAddMultipleAttributeWithMultipleValues()
+    {
+        addAttribute( newEntry, "cn", "1", "2" );
+        addAttribute( newEntry, "member", "cn=1", "cn=2", "cn=3" );
+
+        connection.setModifyMode( ModifyMode.DEFAULT );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ),
+            "add:cn", "cn:1", "cn:2", "-", "add:member", "member:cn=1", "member:cn=2", "member:cn=3" );
+
+        connection.setModifyMode( ModifyMode.REPLACE );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ),
+            "replace:cn", "cn:1", "cn:2", "-", "replace:member", "member:cn=1", "member:cn=2", "member:cn=3" );
+    }
+
+
+    @Test
+    public void shouldAddOneValueToOneExistingAttribute()
+    {
+        addAttribute( oldEntry, "cn", "1" );
+        addAttribute( newEntry, "cn", "1", "2" );
+
+        connection.setModifyMode( ModifyMode.DEFAULT );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ), "add:cn", "cn:2" );
+
+        connection.setModifyMode( ModifyMode.REPLACE );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ), "replace:cn", "cn:1", "cn:2" );
+    }
+
+
+    @Test
+    public void shouldAddMultipleValuesToMultipleExistingAttributes()
+    {
+        addAttribute( oldEntry, "cn", "1" );
+        addAttribute( newEntry, "cn", "1", "2", "3" );
+        addAttribute( oldEntry, "member", "cn=1", "cn=2", "cn=3" );
+        addAttribute( newEntry, "member", "cn=1", "cn=2", "cn=3", "cn=4", "cn=5" );
+
+        connection.setModifyMode( ModifyMode.DEFAULT );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ),
+            "add:cn", "cn:2", "cn:3", "-", "add:member", "member:cn=4", "member:cn=5" );
+
+        connection.setModifyMode( ModifyMode.REPLACE );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ),
+            "replace:cn", "cn:1", "cn:2", "cn:3", "-",
+            "replace:member", "member:cn=1", "member:cn=2", "member:cn=3", "member:cn=4", "member:cn=5" );
+    }
+
+
+    @Test
+    public void shouldDeleteAllOneValue()
+    {
+        addAttribute( oldEntry, "cn", "1" );
+
+        connection.setModifyMode( ModifyMode.DEFAULT );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ), "delete:cn", "cn:1" );
+
+        connection.setModifyMode( ModifyMode.REPLACE );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ), "replace:cn" );
+    }
+
+
+    @Test
+    public void shouldDeleteAllMultipleValues()
+    {
+        addAttribute( oldEntry, "cn", "1", "2" );
+        addAttribute( oldEntry, "member", "cn=1", "cn=2", "cn=3" );
+
+        connection.setModifyMode( ModifyMode.DEFAULT );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ),
+            "delete:cn", "cn:1", "cn:2", "-",
+            "delete:member", "member:cn=1", "member:cn=2", "member:cn=3" );
+
+        connection.setModifyMode( ModifyMode.REPLACE );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ), "replace:cn", "-", "replace:member" );
+    }
+
+
+    @Test
+    public void shouldDeleteOneValue()
+    {
+        addAttribute( oldEntry, "cn", "1", "2", "3" );
+        addAttribute( newEntry, "cn", "1", "2" );
+
+        connection.setModifyMode( ModifyMode.DEFAULT );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ), "delete:cn", "cn:3" );
+
+        connection.setModifyMode( ModifyMode.REPLACE );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ), "replace:cn", "cn:1", "cn:2" );
+    }
+
+
+    @Test
+    public void shouldDeleteMultipleValues()
+    {
+        addAttribute( oldEntry, "cn", "1", "2", "3" );
+        addAttribute( newEntry, "cn", "1" );
+        addAttribute( oldEntry, "member", "cn=1", "cn=2", "cn=3", "cn=4", "cn=5" );
+        addAttribute( newEntry, "member", "cn=1" );
+
+        connection.setModifyMode( ModifyMode.DEFAULT );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ),
+            "delete:cn", "cn:2", "cn:3", "-",
+            "delete:member", "member:cn=2", "member:cn=3", "member:cn=4", "member:cn=5" );
+
+        connection.setModifyMode( ModifyMode.REPLACE );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ),
+            "replace:cn", "cn:1", "-", "replace:member", "member:cn=1" );
+    }
+
+
+    @Test
+    public void shouldReplaceOneValue()
+    {
+        addAttribute( oldEntry, "cn", "1" );
+        addAttribute( newEntry, "cn", "2" );
+
+        connection.setModifyMode( ModifyMode.DEFAULT );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ), "delete:cn", "cn:1", "-", "add:cn", "cn:2" );
+
+        connection.setModifyMode( ModifyMode.REPLACE );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ), "replace:cn", "cn:2" );
+    }
+
+
+    @Test
+    public void shouldReplaceMultipleValues()
+    {
+        addAttribute( oldEntry, "cn", "1", "2", "3" );
+        addAttribute( newEntry, "cn", "4" );
+        addAttribute( oldEntry, "member", "cn=1", "cn=2", "cn=3" );
+        addAttribute( newEntry, "member", "cn=1", "cn=4", "cn=5" );
+
+        connection.setModifyMode( ModifyMode.DEFAULT );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ),
+            "delete:cn", "cn:1", "cn:2", "cn:3", "-", "add:cn", "cn:4", "-",
+            "delete:member", "member:cn=2", "member:cn=3", "-", "add:member", "member:cn=4", "member:cn=5" );
+
+        connection.setModifyMode( ModifyMode.REPLACE );
+        assertChangeModify( Utils.computeDiff( oldEntry, newEntry ),
+            "replace:cn", "cn:4", "-",
+            "replace:member", "member:cn=1", "member:cn=4", "member:cn=5" );
+    }
+
+
+    private static void addAttribute( IEntry entry, String attributeName, Object... rawValues )
+    {
+        Attribute attribute = new Attribute( entry, attributeName );
+        entry.addAttribute( attribute );
+        for ( Object rawValue : rawValues )
+        {
+            Value value = new Value( attribute, rawValue );
+            attribute.addValue( value );
+        }
+    }
+
+
+    private void assertChangeModify( LdifFile diff, String... lines )
+    {
+        // System.out.println( diff.toRawString() );
+        assertThat( diff.isChangeType(), equalTo( true ) );
+        assertThat( diff.getContainers(), hasSize( 1 ) );
+        assertThat( diff.getLastContainer(), instanceOf( LdifChangeModifyRecord.class ) );
+
+        String s = "changetype:modify" + LdifParserConstants.LINE_SEPARATOR;
+        for ( String line : lines )
+        {
+            assertThat( diff.toRawString(), containsString( line ) );
+            s += line + LdifParserConstants.LINE_SEPARATOR;
+        }
+        s += "-" + LdifParserConstants.LINE_SEPARATOR;
+        assertThat( diff.toRawString(), containsString( s ) );
+    }
+
+}
diff --git a/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/EntryEditorTest.java b/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/EntryEditorTest.java
index ae33a71..04fcbec 100644
--- a/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/EntryEditorTest.java
+++ b/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/EntryEditorTest.java
@@ -125,9 +125,6 @@ public class EntryEditorTest extends AbstractLdapTestUnit
 
     /**
      * Test adding, editing and deleting of attributes in the entry editor.
-     *
-     * @throws Exception
-     *             the exception
      */
     @Test
     public void testAddEditDeleteAttribute() throws Exception
@@ -165,7 +162,6 @@ public class EntryEditorTest extends AbstractLdapTestUnit
         entryEditorBot.editValue( "description", "This is the 2nd description." );
         entryEditorBot.typeValueAndFinish( "This is the 3rd description." );
         assertEquals( 10, entryEditorBot.getAttributeValues().size() );
-        assertEquals( 10, entryEditorBot.getAttributeValues().size() );
         assertTrue( entryEditorBot.getAttributeValues().contains( "description: This is the 1st description." ) );
         assertFalse( entryEditorBot.getAttributeValues().contains( "description: This is the 2nd description." ) );
         assertTrue( entryEditorBot.getAttributeValues().contains( "description: This is the 3rd description." ) );
@@ -185,13 +181,14 @@ public class EntryEditorTest extends AbstractLdapTestUnit
         assertEquals( 9, entryEditorBot.getAttributeValues().size() );
         assertFalse( entryEditorBot.getAttributeValues().contains( "description: This is the 1st description." ) );
         assertTrue( entryEditorBot.getAttributeValues().contains( "description: This is the final description." ) );
-        modificationLogsViewBot.waitForText( "replace: description\ndescription: This is the final description." );
+        modificationLogsViewBot.waitForText( "delete: description\ndescription: This is the 1st description." );
+        modificationLogsViewBot.waitForText( "add: description\ndescription: This is the final description." );
 
         // delete 1st value/attribute
         entryEditorBot.deleteValue( "description", "This is the final description." );
         assertEquals( 8, entryEditorBot.getAttributeValues().size() );
         assertFalse( entryEditorBot.getAttributeValues().contains( "description: This is the final description." ) );
-        modificationLogsViewBot.waitForText( "delete: description\n-" );
+        modificationLogsViewBot.waitForText( "delete: description\ndescription: This is the final description.\n-" );
 
         assertEquals( "Expected 6 modifications.", 6,
             StringUtils.countMatches( modificationLogsViewBot.getModificationLogsText(), "#!RESULT OK" ) );
@@ -199,6 +196,55 @@ public class EntryEditorTest extends AbstractLdapTestUnit
 
 
     /**
+     * Test adding, editing and deleting of attributes without equality matching rule in the entry editor.
+     */
+    @Test
+    public void testAddEditDeleteAttributeWithoutEqualityMatchingRule() throws Exception
+    {
+        browserViewBot.selectEntry( "DIT", "Root DSE", "ou=system", "ou=users", "cn=Barbara Jensen" );
+
+        EntryEditorBot entryEditorBot = studioBot.getEntryEditorBot( "cn=Barbara Jensen,ou=users,ou=system" );
+        entryEditorBot.activate();
+        String dn = entryEditorBot.getDnText();
+        assertEquals( "DN: cn=Barbara Jensen,ou=users,ou=system", dn );
+        assertEquals( 8, entryEditorBot.getAttributeValues().size() );
+        assertEquals( "", modificationLogsViewBot.getModificationLogsText() );
+
+        // add facsimileTelephoneNumber attribute
+        entryEditorBot.activate();
+        NewAttributeWizardBot wizardBot = entryEditorBot.openNewAttributeWizard();
+        assertTrue( wizardBot.isVisible() );
+        wizardBot.typeAttributeType( "facsimileTelephoneNumber" );
+        wizardBot.clickFinishButton();
+        entryEditorBot.typeValueAndFinish( "+1 234 567 890" );
+        assertEquals( 9, entryEditorBot.getAttributeValues().size() );
+        assertTrue( entryEditorBot.getAttributeValues().contains( "facsimileTelephoneNumber: +1 234 567 890" ) );
+        modificationLogsViewBot
+            .waitForText( "add: facsimileTelephoneNumber\nfacsimileTelephoneNumber: +1 234 567 890" );
+
+        // edit value
+        entryEditorBot.editValue( "facsimileTelephoneNumber", "+1 234 567 890" );
+        entryEditorBot.typeValueAndFinish( "000000000000" );
+        assertEquals( 9, entryEditorBot.getAttributeValues().size() );
+        assertFalse( entryEditorBot.getAttributeValues().contains( "facsimileTelephoneNumber: +1 234 567 890" ) );
+        assertTrue( entryEditorBot.getAttributeValues().contains( "facsimileTelephoneNumber: 000000000000" ) );
+        modificationLogsViewBot
+            .waitForText( "delete: facsimileTelephoneNumber\nfacsimileTelephoneNumber: +1 234 567 890" );
+        modificationLogsViewBot.waitForText( "add: facsimileTelephoneNumber\nfacsimileTelephoneNumber: 000000000000" );
+
+        // delete 1st value/attribute
+        entryEditorBot.deleteValue( "facsimileTelephoneNumber", "000000000000" );
+        assertEquals( 8, entryEditorBot.getAttributeValues().size() );
+        assertFalse( entryEditorBot.getAttributeValues().contains( "facsimileTelephoneNumber: 000000000000" ) );
+        modificationLogsViewBot
+            .waitForText( "delete: facsimileTelephoneNumber\nfacsimileTelephoneNumber: 000000000000\n-" );
+
+        assertEquals( "Expected 3 modifications.", 3,
+            StringUtils.countMatches( modificationLogsViewBot.getModificationLogsText(), "#!RESULT OK" ) );
+    }
+
+
+    /**
      * DIRSTUDIO-483: DN Editor escapes all non-ascii characters
      *
      * @throws Exception
@@ -471,7 +517,8 @@ public class EntryEditorTest extends AbstractLdapTestUnit
         assertTrue( entryEditorBot.getAttributeValues().contains( "description: " + newValue ) );
         String description2Ldif = LdifAttrValLine.create( "description", newValue )
             .toFormattedString( LdifFormatParameters.DEFAULT ).replace( LdifParserConstants.LINE_SEPARATOR, "\n" );
-        modificationLogsViewBot.waitForText( "replace: description\n" + description2Ldif );
+        modificationLogsViewBot.waitForText( "delete: description\ndescription: testTextValueEditor 1" );
+        modificationLogsViewBot.waitForText( "add: description\n" + description2Ldif );
     }
 
 
@@ -639,7 +686,8 @@ public class EntryEditorTest extends AbstractLdapTestUnit
         aciItemEditor.clickOkButton();
 
         SWTUtils.sleep( 1000 );
-        modificationLogsViewBot.waitForText( "replace: entryaci\n" );
+        modificationLogsViewBot.waitForText( "delete: entryaci\n" );
+        modificationLogsViewBot.waitForText( "add: entryaci\n" );
     }
 
 
@@ -678,7 +726,8 @@ public class EntryEditorTest extends AbstractLdapTestUnit
         aciItemEditor.clickOkButton();
 
         SWTUtils.sleep( 1000 );
-        modificationLogsViewBot.waitForText( "replace: entryaci\n" );
+        modificationLogsViewBot.waitForText( "delete: entryaci\n" );
+        modificationLogsViewBot.waitForText( "add: entryaci\n" );
     }
 
 
@@ -706,7 +755,8 @@ public class EntryEditorTest extends AbstractLdapTestUnit
         aciItemEditor.clickOkButton();
 
         SWTUtils.sleep( 1000 );
-        modificationLogsViewBot.waitForText( "replace: prescriptiveaci\n" );
+        modificationLogsViewBot.waitForText( "delete: prescriptiveaci\n" );
+        modificationLogsViewBot.waitForText( "add: prescriptiveaci\n" );
     }
 
 
diff --git a/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/ErrorHandlingTest.java b/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/ErrorHandlingTest.java
index 0f3dd30..6b302a8 100644
--- a/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/ErrorHandlingTest.java
+++ b/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/ErrorHandlingTest.java
@@ -195,7 +195,8 @@ public class ErrorHandlingTest extends AbstractLdapTestUnit
 
         // verify in modification logs
         modificationLogsViewBot.assertContainsError( "[LDAP result code 21 - invalidAttributeSyntax]",
-            "dn: uid=user.1,ou=users,ou=system", "changetype: modify", "replace: telephonenumber",
+            "dn: uid=user.1,ou=users,ou=system", "changetype: modify", "delete: telephonenumber",
+            "telephonenumber: 976-893-3312", "-", "add: telephonenumber",
             "telephonenumber: Invalid phone number" );
     }
 
diff --git a/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/OpenLdapTest.java b/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/OpenLdapTest.java
index 72d94f4..0eae6d1 100644
--- a/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/OpenLdapTest.java
+++ b/tests/test.integration.ui/src/main/java/org/apache/directory/studio/test/integration/ui/OpenLdapTest.java
@@ -24,6 +24,7 @@ package org.apache.directory.studio.test.integration.ui;
 import static org.apache.directory.studio.test.integration.ui.Constants.LOCALHOST;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -33,15 +34,21 @@ import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
 import org.apache.directory.api.ldap.model.ldif.LdifEntry;
 import org.apache.directory.api.ldap.model.ldif.LdifReader;
 import org.apache.directory.ldap.client.api.LdapNetworkConnection;
 import org.apache.directory.ldap.client.api.exception.InvalidConnectionException;
 import org.apache.directory.studio.connection.core.Connection;
+import org.apache.directory.studio.ldapbrowser.core.BrowserCorePlugin;
 import org.apache.directory.studio.ldapbrowser.core.model.IBrowserConnection;
+import org.apache.directory.studio.ldapbrowser.core.model.IBrowserConnection.ModifyMode;
 import org.apache.directory.studio.test.integration.ui.bots.BrowserViewBot;
 import org.apache.directory.studio.test.integration.ui.bots.ConnectionsViewBot;
+import org.apache.directory.studio.test.integration.ui.bots.EntryEditorBot;
+import org.apache.directory.studio.test.integration.ui.bots.ModificationLogsViewBot;
+import org.apache.directory.studio.test.integration.ui.bots.NewAttributeWizardBot;
 import org.apache.directory.studio.test.integration.ui.bots.NewConnectionWizardBot;
 import org.apache.directory.studio.test.integration.ui.bots.SearchDialogBot;
 import org.apache.directory.studio.test.integration.ui.bots.StudioBot;
@@ -101,6 +108,7 @@ public class OpenLdapTest
     private StudioBot studioBot;
     private ConnectionsViewBot connectionsViewBot;
     private BrowserViewBot browserViewBot;
+    private ModificationLogsViewBot modificationLogsViewBot;
 
     private Connection connection;
 
@@ -113,6 +121,7 @@ public class OpenLdapTest
         connection = connectionsViewBot.createTestConnection( "OpenLdapTest", OPENLDAP_HOST, OPENLDAP_PORT,
             OPENLDAP_ADMIN_DN, OPENLDAP_ADMIN_PASSWORD );
         browserViewBot = studioBot.getBrowserView();
+        modificationLogsViewBot = studioBot.getModificationLogsViewBot();
 
         try ( LdapNetworkConnection connection = new LdapNetworkConnection( OPENLDAP_HOST, OPENLDAP_PORT );
             LdifReader ldifReader = new LdifReader( OpenLdapTest.class.getResourceAsStream( "OpenLdapTest.ldif" ) ) )
@@ -293,4 +302,128 @@ public class OpenLdapTest
         wizardBot.clickCancelButton();
     }
 
+
+    /**
+     * Test adding, editing and deleting of attributes in the entry editor.
+     */
+    @Test
+    public void testAddEditDeleteAttribute() throws Exception
+    {
+        browserViewBot.selectEntry( "DIT", "Root DSE", "dc=example,dc=org", "ou=users", "uid=user.1" );
+
+        EntryEditorBot entryEditorBot = studioBot.getEntryEditorBot( "uid=user.1,ou=users,dc=example,dc=org" );
+        entryEditorBot.activate();
+        String dn = entryEditorBot.getDnText();
+        assertEquals( "DN: uid=user.1,ou=users,dc=example,dc=org", dn );
+        assertEquals( 22, entryEditorBot.getAttributeValues().size() );
+        assertEquals( "", modificationLogsViewBot.getModificationLogsText() );
+
+        // add description attribute
+        entryEditorBot.activate();
+        NewAttributeWizardBot wizardBot = entryEditorBot.openNewAttributeWizard();
+        assertTrue( wizardBot.isVisible() );
+        wizardBot.typeAttributeType( "description" );
+        wizardBot.clickFinishButton();
+        entryEditorBot.typeValueAndFinish( "This is the 1st description." );
+        assertEquals( 23, entryEditorBot.getAttributeValues().size() );
+        assertTrue( entryEditorBot.getAttributeValues().contains( "description: This is the 1st description." ) );
+        modificationLogsViewBot.waitForText( "add: description\ndescription: This is the 1st description." );
+
+        // add second value
+        entryEditorBot.activate();
+        entryEditorBot.addValue( "description" );
+        entryEditorBot.typeValueAndFinish( "This is the 2nd description." );
+        assertEquals( 24, entryEditorBot.getAttributeValues().size() );
+        assertTrue( entryEditorBot.getAttributeValues().contains( "description: This is the 1st description." ) );
+        assertTrue( entryEditorBot.getAttributeValues().contains( "description: This is the 2nd description." ) );
+        modificationLogsViewBot.waitForText( "add: description\ndescription: This is the 2nd description." );
+
+        // edit second value
+        entryEditorBot.editValue( "description", "This is the 2nd description." );
+        entryEditorBot.typeValueAndFinish( "This is the 3rd description." );
+        assertEquals( 24, entryEditorBot.getAttributeValues().size() );
+        assertTrue( entryEditorBot.getAttributeValues().contains( "description: This is the 1st description." ) );
+        assertFalse( entryEditorBot.getAttributeValues().contains( "description: This is the 2nd description." ) );
+        assertTrue( entryEditorBot.getAttributeValues().contains( "description: This is the 3rd description." ) );
+        modificationLogsViewBot.waitForText( "delete: description\ndescription: This is the 2nd description." );
+        modificationLogsViewBot.waitForText( "add: description\ndescription: This is the 3rd description." );
+
+        // delete second value
+        entryEditorBot.deleteValue( "description", "This is the 3rd description." );
+        assertEquals( 23, entryEditorBot.getAttributeValues().size() );
+        assertTrue( entryEditorBot.getAttributeValues().contains( "description: This is the 1st description." ) );
+        assertFalse( entryEditorBot.getAttributeValues().contains( "description: This is the 3rd description." ) );
+        modificationLogsViewBot.waitForText( "delete: description\ndescription: This is the 3rd description." );
+
+        // edit 1st value
+        entryEditorBot.editValue( "description", "This is the 1st description." );
+        entryEditorBot.typeValueAndFinish( "This is the final description." );
+        assertEquals( 23, entryEditorBot.getAttributeValues().size() );
+        assertFalse( entryEditorBot.getAttributeValues().contains( "description: This is the 1st description." ) );
+        assertTrue( entryEditorBot.getAttributeValues().contains( "description: This is the final description." ) );
+        modificationLogsViewBot.waitForText( "delete: description\ndescription: This is the 1st description." );
+        modificationLogsViewBot.waitForText( "add: description\ndescription: This is the final description." );
+
+        // delete 1st value/attribute
+        entryEditorBot.deleteValue( "description", "This is the final description." );
+        assertEquals( 22, entryEditorBot.getAttributeValues().size() );
+        assertFalse( entryEditorBot.getAttributeValues().contains( "description: This is the final description." ) );
+        modificationLogsViewBot.waitForText( "delete: description\ndescription: This is the final description.\n-" );
+
+        assertEquals( "Expected 6 modifications.", 6,
+            StringUtils.countMatches( modificationLogsViewBot.getModificationLogsText(), "#!RESULT OK" ) );
+    }
+
+
+    /**
+     * Test adding, editing and deleting of attributes without equality matching rule in the entry editor.
+     */
+    @Test
+    public void testAddEditDeleteAttributeWithoutEqualityMatchingRule() throws Exception
+    {
+        IBrowserConnection browserConnection = BrowserCorePlugin.getDefault().getConnectionManager()
+            .getBrowserConnection( connection );
+        browserConnection.setModifyModeNoEMR( ModifyMode.REPLACE );
+
+        browserViewBot.selectEntry( "DIT", "Root DSE", "dc=example,dc=org", "ou=users", "uid=user.1" );
+
+        EntryEditorBot entryEditorBot = studioBot.getEntryEditorBot( "uid=user.1,ou=users,dc=example,dc=org" );
+        entryEditorBot.activate();
+        String dn = entryEditorBot.getDnText();
+        assertEquals( "DN: uid=user.1,ou=users,dc=example,dc=org", dn );
+        assertEquals( 22, entryEditorBot.getAttributeValues().size() );
+        assertEquals( "", modificationLogsViewBot.getModificationLogsText() );
+
+        // add facsimileTelephoneNumber attribute
+        entryEditorBot.activate();
+        NewAttributeWizardBot wizardBot = entryEditorBot.openNewAttributeWizard();
+        assertTrue( wizardBot.isVisible() );
+        wizardBot.typeAttributeType( "facsimileTelephoneNumber" );
+        wizardBot.clickFinishButton();
+        entryEditorBot.typeValueAndFinish( "+1 234 567 890" );
+        assertEquals( 23, entryEditorBot.getAttributeValues().size() );
+        assertTrue( entryEditorBot.getAttributeValues().contains( "facsimileTelephoneNumber: +1 234 567 890" ) );
+        modificationLogsViewBot
+            .waitForText( "replace: facsimileTelephoneNumber\nfacsimileTelephoneNumber: +1 234 567 890" );
+
+        // edit value
+        entryEditorBot.editValue( "facsimileTelephoneNumber", "+1 234 567 890" );
+        entryEditorBot.typeValueAndFinish( "000000000000" );
+        assertEquals( 23, entryEditorBot.getAttributeValues().size() );
+        assertFalse( entryEditorBot.getAttributeValues().contains( "facsimileTelephoneNumber: +1 234 567 890" ) );
+        assertTrue( entryEditorBot.getAttributeValues().contains( "facsimileTelephoneNumber: 000000000000" ) );
+        modificationLogsViewBot
+            .waitForText( "replace: facsimileTelephoneNumber\nfacsimileTelephoneNumber: 000000000000" );
+
+        // delete 1st value/attribute
+        entryEditorBot.deleteValue( "facsimileTelephoneNumber", "000000000000" );
+        assertEquals( 22, entryEditorBot.getAttributeValues().size() );
+        assertFalse( entryEditorBot.getAttributeValues().contains( "facsimileTelephoneNumber: 000000000000" ) );
+        modificationLogsViewBot
+            .waitForText( "replace: facsimileTelephoneNumber\n-" );
+
+        assertEquals( "Expected 3 modifications.", 3,
+            StringUtils.countMatches( modificationLogsViewBot.getModificationLogsText(), "#!RESULT OK" ) );
+    }
+
 }
diff --git a/tests/test.integration.ui/src/main/resources/org/apache/directory/studio/test/integration/ui/OpenLdapTest.ldif b/tests/test.integration.ui/src/main/resources/org/apache/directory/studio/test/integration/ui/OpenLdapTest.ldif
index d64ed89..4e39b2f 100644
--- a/tests/test.integration.ui/src/main/resources/org/apache/directory/studio/test/integration/ui/OpenLdapTest.ldif
+++ b/tests/test.integration.ui/src/main/resources/org/apache/directory/studio/test/integration/ui/OpenLdapTest.ldif
@@ -42,7 +42,6 @@ l: Tallahassee
 st: DE
 postalCode: 67698
 postalAddress: Aaccf Amar$27919 Broadway Street$Tallahassee, DE  67698
-description: This is the description for Aaccf Amar.
 roomNumber: 1388
 
 dn: uid=user.2,ou=users,dc=example,dc=org