JAMES-1951 add helper class UidMsnConverter

Project: http://git-wip-us.apache.org/repos/asf/james-project/repo
Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/3f0aa543
Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/3f0aa543
Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/3f0aa543

Branch: refs/heads/master
Commit: 3f0aa543b8980f8f8d47ccfe89859d4c3cf960d3
Parents: c090b75
Author: benwa <btell...@linagora.com>
Authored: Thu Jun 8 14:46:24 2017 +0700
Committer: Antoine Duprat <adup...@linagora.com>
Committed: Fri Jun 9 21:56:32 2017 +0200

----------------------------------------------------------------------
 protocols/imap/pom.xml                          |   5 +
 .../imap/processor/base/UidMsnConverter.java    | 106 ++++++
 .../processor/base/UidMsnConverterTest.java     | 370 +++++++++++++++++++
 3 files changed, 481 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/3f0aa543/protocols/imap/pom.xml
----------------------------------------------------------------------
diff --git a/protocols/imap/pom.xml b/protocols/imap/pom.xml
index 6c1ea0c..c2f590d 100644
--- a/protocols/imap/pom.xml
+++ b/protocols/imap/pom.xml
@@ -60,6 +60,11 @@
         </dependency>
         <dependency>
             <groupId>org.apache.james</groupId>
+            <artifactId>james-server-util</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.james</groupId>
             <artifactId>metrics-api</artifactId>
         </dependency>
         <dependency>

http://git-wip-us.apache.org/repos/asf/james-project/blob/3f0aa543/protocols/imap/src/main/java/org/apache/james/imap/processor/base/UidMsnConverter.java
----------------------------------------------------------------------
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/base/UidMsnConverter.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/base/UidMsnConverter.java
new file mode 100644
index 0000000..9276e27
--- /dev/null
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/base/UidMsnConverter.java
@@ -0,0 +1,106 @@
+/****************************************************************
+ * 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.james.imap.processor.base;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+
+import org.apache.james.mailbox.MessageUid;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
+
+public class UidMsnConverter {
+
+    public static final int FIRST_MSN = 1;
+
+    @VisibleForTesting final ArrayList<MessageUid> uids;
+
+    public UidMsnConverter(Iterator<MessageUid> iterator) {
+        uids = Lists.newArrayList(iterator);
+        Collections.sort(uids);
+    }
+
+    public synchronized Optional<Integer> getMsn(MessageUid uid) {
+        int position = Collections.binarySearch(uids, uid);
+        if (position < 0) {
+            return Optional.absent();
+        }
+        return Optional.of(position + 1);
+    }
+
+    public synchronized Optional<MessageUid> getUid(int msn) {
+        if (msn <= uids.size() && msn > 0) {
+            return Optional.of(uids.get(msn - 1));
+        }
+        return Optional.absent();
+    }
+
+    public synchronized Optional<MessageUid> getLastUid() {
+        if (uids.isEmpty()) {
+            return Optional.absent();
+        }
+        return getUid(getLastMsn());
+    }
+
+    public synchronized Optional<MessageUid> getFirstUid() {
+        return getUid(FIRST_MSN);
+    }
+
+    public synchronized int getNumMessage() {
+        return uids.size();
+    }
+
+    public synchronized void remove(MessageUid uid) {
+        uids.remove(uid);
+    }
+
+    public synchronized boolean isEmpty() {
+        return uids.isEmpty();
+    }
+
+    public synchronized void clear() {
+        uids.clear();
+    }
+
+    public synchronized void addUid(MessageUid uid) {
+        if (uids.contains(uid)) {
+            return;
+        }
+        if (isLastUid(uid)) {
+            uids.add(uid);
+        } else {
+            uids.add(uid);
+            Collections.sort(uids);
+        }
+    }
+
+    private boolean isLastUid(MessageUid uid) {
+        Optional<MessageUid> lastUid = getLastUid();
+        return !lastUid.isPresent() ||
+            lastUid.get().compareTo(uid) < 0;
+    }
+
+    private int getLastMsn() {
+        return getNumMessage();
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/3f0aa543/protocols/imap/src/test/java/org/apache/james/imap/processor/base/UidMsnConverterTest.java
----------------------------------------------------------------------
diff --git 
a/protocols/imap/src/test/java/org/apache/james/imap/processor/base/UidMsnConverterTest.java
 
b/protocols/imap/src/test/java/org/apache/james/imap/processor/base/UidMsnConverterTest.java
new file mode 100644
index 0000000..4606e83
--- /dev/null
+++ 
b/protocols/imap/src/test/java/org/apache/james/imap/processor/base/UidMsnConverterTest.java
@@ -0,0 +1,370 @@
+/****************************************************************
+ * 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.james.imap.processor.base;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.guava.api.Assertions.assertThat;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.james.mailbox.MessageUid;
+import org.apache.james.util.concurrency.ConcurrentTestRunner;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class UidMsnConverterTest {
+    private UidMsnConverter testee;
+    private MessageUid messageUid1;
+    private MessageUid messageUid2;
+    private MessageUid messageUid3;
+    private MessageUid messageUid4;
+
+    @Before
+    public void setUp() {
+        testee = new 
UidMsnConverter(ImmutableList.<MessageUid>of().iterator());
+        messageUid1 = MessageUid.of(1);
+        messageUid2 = MessageUid.of(2);
+        messageUid3 = MessageUid.of(3);
+        messageUid4 = MessageUid.of(4);
+    }
+
+    @Test
+    public void getUidShouldReturnEmptyIfNoMessageWithTheGivenMessageNumber() {
+        assertThat(testee.getUid(1))
+            .isAbsent();
+    }
+
+    @Test
+    public void getUidShouldReturnEmptyIfZero() {
+        assertThat(testee.getUid(0))
+            .isAbsent();
+    }
+
+    @Test
+    public void getUidShouldTheCorrespondingUidIfItExist() {
+        testee.addUid(messageUid1);
+
+        assertThat(testee.getUid(1))
+            .contains(messageUid1);
+    }
+
+    @Test
+    public void getFirstUidShouldReturnEmptyIfNoMessage() {
+        assertThat(testee.getFirstUid()).isAbsent();
+    }
+
+    @Test
+    public void getLastUidShouldReturnEmptyIfNoMessage() {
+        assertThat(testee.getLastUid()).isAbsent();
+    }
+
+    @Test
+    public void getFirstUidShouldReturnFirstUidIfAtLeastOneMessage() {
+        testee.addUid(messageUid1);
+        testee.addUid(messageUid2);
+
+        assertThat(testee.getFirstUid()).contains(messageUid1);
+    }
+
+    @Test
+    public void getLastUidShouldReturnLastUidIfAtLeastOneMessage() {
+        testee.addUid(messageUid1);
+        testee.addUid(messageUid2);
+
+        assertThat(testee.getLastUid()).contains(messageUid2);
+    }
+
+    @Test
+    public void getMsnShouldReturnAbsentIfNoCorrespondingMessage() {
+        testee.addUid(messageUid1);
+
+        assertThat(testee.getMsn(messageUid2)).isAbsent();
+    }
+
+    @Test
+    public void getMsnShouldReturnMessageNumberIfUidIsThere() {
+        testee.addUid(messageUid1);
+        testee.addUid(messageUid2);
+
+        assertThat(testee.getMsn(messageUid2))
+            .contains(2);
+    }
+
+    @Test
+    public void getNumMessageShouldReturnZeroIfNoMapping() {
+        assertThat(testee.getNumMessage())
+            .isEqualTo(0);
+    }
+
+    @Test
+    public void getNumMessageShouldReturnTheNumOfMessage() {
+        testee.addUid(messageUid1);
+        testee.addUid(messageUid2);
+
+        assertThat(testee.getNumMessage())
+            .isEqualTo(2);
+    }
+
+    @Test
+    public void isEmptyShouldReturnTrueIfNoMapping() {
+        assertThat(testee.isEmpty())
+            .isTrue();
+    }
+
+    @Test
+    public void isEmptyShouldReturnFalseIfMapping() {
+        testee.addUid(messageUid1);
+
+        assertThat(testee.isEmpty())
+            .isFalse();
+    }
+
+    @Test
+    public void clearShouldClearMapping() {
+        testee.addUid(messageUid1);
+
+        testee.clear();
+
+        assertThat(testee.isEmpty())
+            .isTrue();
+    }
+
+    @Test
+    public void addUidShouldKeepMessageNumberContiguous() {
+        testee.addUid(messageUid1);
+        testee.addUid(messageUid2);
+        testee.addUid(messageUid3);
+        testee.addUid(messageUid4);
+        testee.addUid(messageUid2);
+
+        assertThat(mapTesteeInternalDataToMsnByUid())
+            .isEqualTo(ImmutableMap.of(
+                1, messageUid1,
+                2, messageUid2,
+                3, messageUid3,
+                4, messageUid4));
+    }
+
+    @Test
+    public void addUidShouldNotOverridePreviousMapping() {
+        testee.addUid(messageUid1);
+        testee.addUid(messageUid2);
+        testee.addUid(messageUid3);
+        testee.addUid(messageUid2);
+
+        assertThat(testee.getMsn(messageUid2))
+            .contains(2);
+    }
+
+    @Test
+    public void removeShouldKeepAValidMappingWhenDeletingBeginning() {
+        testee.addUid(messageUid1);
+        testee.addUid(messageUid2);
+        testee.addUid(messageUid3);
+        testee.addUid(messageUid4);
+
+        testee.remove(messageUid1);
+
+        assertThat(mapTesteeInternalDataToMsnByUid())
+            .isEqualTo(ImmutableMap.of(
+                1, messageUid2,
+                2, messageUid3,
+                3, messageUid4));
+    }
+
+    @Test
+    public void removeShouldKeepAValidMappingWhenDeletingEnd() {
+        testee.addUid(messageUid1);
+        testee.addUid(messageUid2);
+        testee.addUid(messageUid3);
+        testee.addUid(messageUid4);
+
+        testee.remove(messageUid4);
+
+        assertThat(mapTesteeInternalDataToMsnByUid())
+            .isEqualTo(ImmutableMap.of(
+                1, messageUid1,
+                2, messageUid2,
+                3, messageUid3));
+    }
+
+    @Test
+    public void removeShouldKeepAValidMappingWhenDeletingMiddle() {
+        testee.addUid(messageUid1);
+        testee.addUid(messageUid2);
+        testee.addUid(messageUid3);
+        testee.addUid(messageUid4);
+
+        testee.remove(messageUid3);
+
+        assertThat(mapTesteeInternalDataToMsnByUid())
+            .isEqualTo(ImmutableMap.of(
+                1, messageUid1,
+                2, messageUid2,
+                3, messageUid4));
+    }
+
+    @Test
+    public void addUidShouldSupportOutOfOrderUpdates() {
+        testee.addUid(messageUid1);
+        testee.addUid(messageUid3);
+        testee.addUid(messageUid2);
+        testee.addUid(messageUid4);
+
+        assertThat(mapTesteeInternalDataToMsnByUid().entrySet())
+            .containsOnlyElementsOf(ImmutableMap.of(
+                1, messageUid1,
+                2, messageUid2,
+                3, messageUid3,
+                4, messageUid4).entrySet());
+    }
+
+    @Test
+    public void addUidShouldLeadToValidConversionWhenInsertInFirstPosition() {
+        testee.addUid(messageUid2);
+        testee.addUid(messageUid3);
+        testee.addUid(messageUid4);
+        testee.addUid(messageUid1);
+
+        assertThat(mapTesteeInternalDataToMsnByUid().entrySet())
+            .containsOnlyElementsOf(ImmutableMap.of(
+                1, messageUid1,
+                2, messageUid2,
+                3, messageUid3,
+                4, messageUid4).entrySet());
+    }
+
+    @Test
+    public void constructorWithOutOfOrderIteratorShouldLeadToValidConversion() 
{
+        testee = new UidMsnConverter(ImmutableList.of(messageUid2,
+            messageUid3,
+            messageUid4,
+            messageUid1)
+            .iterator());
+
+        assertThat(mapTesteeInternalDataToMsnByUid().entrySet())
+            .containsOnlyElementsOf(ImmutableMap.of(
+                1, messageUid1,
+                2, messageUid2,
+                3, messageUid3,
+                4, messageUid4).entrySet());
+    }
+
+    @Test
+    public void addUidShouldBeIdempotent() {
+        testee.addUid(messageUid1);
+        testee.addUid(messageUid1);
+
+        assertThat(mapTesteeInternalDataToMsnByUid())
+            .isEqualTo(ImmutableMap.of(1, messageUid1));
+    }
+
+    @Test
+    public void addAndRemoveShouldLeadToValidConversionWhenMixed() throws 
Exception {
+        final int initialCount = 1000;
+        for (int i = 1; i <= initialCount; i++) {
+            testee.addUid(MessageUid.of(i));
+        }
+
+        int threadCount = 2;
+        ConcurrentTestRunner concurrentTestRunner = new 
ConcurrentTestRunner(threadCount, initialCount,
+            new ConcurrentTestRunner.BiConsumer() {
+                @Override
+                public void consume(int threadNumber, int step) throws 
Exception {
+                    if (threadNumber == 0) {
+                        testee.remove(MessageUid.of(step + 1));
+                    } else {
+                        testee.addUid(MessageUid.of(initialCount + step + 1));
+                    }
+                }
+            });
+        concurrentTestRunner.run();
+        concurrentTestRunner.awaitTermination(10, TimeUnit.SECONDS);
+
+        ImmutableMap.Builder<Integer, MessageUid> resultBuilder = 
ImmutableMap.builder();
+        for (int i = 1; i <= initialCount; i++) {
+            resultBuilder.put(i, MessageUid.of(initialCount + i));
+        }
+        assertThat(mapTesteeInternalDataToMsnByUid().entrySet())
+            .containsOnlyElementsOf(resultBuilder.build().entrySet());
+    }
+
+    @Test
+    public void addShouldLeadToValidConversionWhenConcurrent() throws 
Exception {
+        final int operationCount = 1000;
+        int threadCount = 2;
+
+        ConcurrentTestRunner concurrentTestRunner = new 
ConcurrentTestRunner(threadCount, operationCount,
+            new ConcurrentTestRunner.BiConsumer() {
+                @Override
+                public void consume(int threadNumber, int step) throws 
Exception {
+                    testee.addUid(MessageUid.of((threadNumber * 
operationCount) + (step + 1)));
+                }
+            });
+        concurrentTestRunner.run();
+        concurrentTestRunner.awaitTermination(10, TimeUnit.SECONDS);
+
+        ImmutableMap.Builder<Integer, MessageUid> resultBuilder = 
ImmutableMap.builder();
+        for (int i = 1; i <= threadCount * operationCount; i++) {
+            resultBuilder.put(i, MessageUid.of(i));
+        }
+        assertThat(mapTesteeInternalDataToMsnByUid().entrySet())
+            .containsOnlyElementsOf(resultBuilder.build().entrySet());
+    }
+
+    @Test
+    public void removeShouldLeadToValidConversionWhenConcurrent() throws 
Exception {
+        final int operationCount = 1000;
+        int threadCount = 2;
+        for (int i = 1; i <= operationCount * (threadCount + 1); i++) {
+            testee.addUid(MessageUid.of(i));
+        }
+
+        ConcurrentTestRunner concurrentTestRunner = new 
ConcurrentTestRunner(threadCount, operationCount,
+            new ConcurrentTestRunner.BiConsumer() {
+                @Override
+                public void consume(int threadNumber, int step) throws 
Exception {
+                    testee.remove(MessageUid.of((threadNumber * 
operationCount) + (step + 1)));
+                }
+            });
+        concurrentTestRunner.run();
+        concurrentTestRunner.awaitTermination(10, TimeUnit.SECONDS);
+
+        ImmutableMap.Builder<Integer, MessageUid> resultBuilder = 
ImmutableMap.builder();
+        for (int i = 1; i <= operationCount; i++) {
+            resultBuilder.put(i, MessageUid.of((threadCount * operationCount) 
+ i));
+        }
+        assertThat(mapTesteeInternalDataToMsnByUid().entrySet())
+            .containsOnlyElementsOf(resultBuilder.build().entrySet());
+    }
+
+    private Map<Integer, MessageUid> mapTesteeInternalDataToMsnByUid() {
+        ImmutableMap.Builder<Integer, MessageUid> result = 
ImmutableMap.builder();
+        for (int i = 0; i < testee.uids.size(); i++) {
+            result.put(i + 1, testee.uids.get(i));
+        }
+        return result.build();
+    }
+
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org
For additional commands, e-mail: server-dev-h...@james.apache.org

Reply via email to