chibenwa commented on a change in pull request #707:
URL: https://github.com/apache/james-project/pull/707#discussion_r735656054



##########
File path: 
server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/UuidState.scala
##########
@@ -0,0 +1,60 @@
+/******************************************************************
+ * 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.jmap.api.model
+
+import java.util.UUID
+
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.refineV
+import eu.timepit.refined.string.Uuid
+import org.apache.james.jmap.api.change.{State => JavaState, EmailChanges, 
MailboxChanges}
+
+import scala.util.Try
+
+object UuidState {

Review comment:
       Why do we need UuidState here ? (question)

##########
File path: 
server/data/data-jmap/src/main/java/org/apache/james/jmap/api/pushsubscription/PushSubscriptionVerificationCodeFactory.java
##########
@@ -0,0 +1,38 @@
+/******************************************************************
+ * 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.jmap.api.pushsubscription;
+
+import java.util.UUID;
+
+import org.apache.james.jmap.api.model.VerificationCode;
+
+public interface PushSubscriptionVerificationCodeFactory {

Review comment:
       I do not get the advantage of this class other a simple factory method.
   
   ```
   class VerificationCode {
      public static VerificationCodegenerate {...}
      
      // ... the rest of VerificationCode class.
   }

##########
File path: 
server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
##########
@@ -0,0 +1,163 @@
+/******************************************************************
+ * 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.jmap.memory.pushsubscription;
+
+import java.time.Clock;
+import java.time.ZonedDateTime;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.DeviceClientIdInvalidException;
+import org.apache.james.jmap.api.model.PushSubscription;
+import org.apache.james.jmap.api.model.PushSubscriptionCreationRequest;
+import org.apache.james.jmap.api.model.PushSubscriptionExpiredTime;
+import org.apache.james.jmap.api.model.PushSubscriptionId;
+import org.apache.james.jmap.api.model.PushSubscriptionNotFoundException;
+import org.apache.james.jmap.api.model.TypeName;
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository;
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionVerificationCodeFactory;
+import org.reactivestreams.Publisher;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import scala.jdk.javaapi.CollectionConverters;
+import scala.jdk.javaapi.OptionConverters;
+
+public class MemoryPushSubscriptionRepository implements 
PushSubscriptionRepository {
+    private final Table<Username, PushSubscriptionId, PushSubscription> table;
+    private final Clock clock;
+    private final PushSubscriptionVerificationCodeFactory 
verificationCodeFactory;
+
+    @Inject
+    public MemoryPushSubscriptionRepository(Clock clock, 
PushSubscriptionVerificationCodeFactory factory) {
+        this.clock = clock;
+        this.verificationCodeFactory = factory;
+        table = HashBasedTable.create();
+    }
+
+    @Override
+    public Publisher<PushSubscriptionId> save(Username username, 
PushSubscriptionCreationRequest request) {
+        if (request.expires().isDefined()) {
+            expireTimePreconditions(request.expires().get().value(), clock);
+        }
+        if (!isUniqueDeviceClientId(username, request.deviceClientId())) {
+            throw new DeviceClientIdInvalidException(request.deviceClientId(), 
"deviceClientId must be unique");

Review comment:
       Mono.error IMO

##########
File path: 
server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
##########
@@ -0,0 +1,163 @@
+/******************************************************************
+ * 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.jmap.memory.pushsubscription;
+
+import java.time.Clock;
+import java.time.ZonedDateTime;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.DeviceClientIdInvalidException;
+import org.apache.james.jmap.api.model.PushSubscription;
+import org.apache.james.jmap.api.model.PushSubscriptionCreationRequest;
+import org.apache.james.jmap.api.model.PushSubscriptionExpiredTime;
+import org.apache.james.jmap.api.model.PushSubscriptionId;
+import org.apache.james.jmap.api.model.PushSubscriptionNotFoundException;
+import org.apache.james.jmap.api.model.TypeName;
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository;
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionVerificationCodeFactory;
+import org.reactivestreams.Publisher;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import scala.jdk.javaapi.CollectionConverters;
+import scala.jdk.javaapi.OptionConverters;
+
+public class MemoryPushSubscriptionRepository implements 
PushSubscriptionRepository {
+    private final Table<Username, PushSubscriptionId, PushSubscription> table;
+    private final Clock clock;
+    private final PushSubscriptionVerificationCodeFactory 
verificationCodeFactory;
+
+    @Inject
+    public MemoryPushSubscriptionRepository(Clock clock, 
PushSubscriptionVerificationCodeFactory factory) {
+        this.clock = clock;
+        this.verificationCodeFactory = factory;
+        table = HashBasedTable.create();
+    }
+
+    @Override
+    public Publisher<PushSubscriptionId> save(Username username, 
PushSubscriptionCreationRequest request) {
+        if (request.expires().isDefined()) {
+            expireTimePreconditions(request.expires().get().value(), clock);
+        }
+        if (!isUniqueDeviceClientId(username, request.deviceClientId())) {
+            throw new DeviceClientIdInvalidException(request.deviceClientId(), 
"deviceClientId must be unique");
+        }
+        return Mono.fromCallable(() -> 
PushSubscriptionId.apply(UUID.randomUUID()))
+            .doOnNext(id -> table.put(username, id,
+                PushSubscription.from(request,
+                    id,
+                    
evaluateExpiresTime(OptionConverters.toJava(request.expires().map(PushSubscriptionExpiredTime::value)),
 clock),
+                    String.valueOf(verificationCodeFactory.generate()))));
+    }
+
+    @Override
+    public Publisher<Void> updateExpireTime(Username username, 
PushSubscriptionId id, ZonedDateTime newExpire) {
+        expireTimePreconditions(newExpire, clock);
+        return Mono.justOrEmpty(table.get(username, id))
+            .doOnNext(pushSubscription -> {
+                PushSubscription newPushSubscription = PushSubscription.apply(
+                    pushSubscription.id(),
+                    pushSubscription.deviceClientId(),
+                    pushSubscription.url(),
+                    pushSubscription.keys(),
+                    pushSubscription.verificationCode(),
+                    pushSubscription.validated(),
+                    
PushSubscriptionExpiredTime.apply(evaluateExpiresTime(Optional.of(newExpire), 
clock)),
+                    pushSubscription.types());

Review comment:
       Too easy too mix stuff in these long declarations, too boring to read.
   
   How about writing something like:
   
   `pushSubsciption.withExpireDate(expireDate)`

##########
File path: 
server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushSubscriptionRepositoryContract.scala
##########
@@ -0,0 +1,174 @@
+/** ****************************************************************
+ * 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.jmap.api.pushsubscription
+
+import java.net.URL
+import java.time.{Clock, Instant, ZoneId, ZonedDateTime}
+
+import org.apache.james.core.Username
+import org.apache.james.jmap.api.model.{DeviceClientId, 
DeviceClientIdInvalidException, EmailTypeName, ExpireTimeInvalidException, 
PushSubscriptionCreationRequest, PushSubscriptionExpiredTime, 
PushSubscriptionServerURL}
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepositoryContract.{ALICE,
 BIGGER_EXPIRE_THAN_MAX, INVALID_EXPIRE, MAX_EXPIRE}
+import org.assertj.core.api.Assertions.{assertThat, assertThatThrownBy}
+import org.junit.jupiter.api.Test
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+import scala.jdk.CollectionConverters._
+
+object PushSubscriptionRepositoryContract {
+  val NOW: Instant = Instant.parse("2021-10-25T07:05:39.160Z")
+  val CLOCK: Clock = Clock.fixed(NOW, ZoneId.of("UTC"))
+  val INVALID_EXPIRE: ZonedDateTime = 
ZonedDateTime.parse("2020-10-25T07:05:39.160Z[UTC]")
+  val MAX_EXPIRE: ZonedDateTime = 
ZonedDateTime.parse("2021-10-25T07:05:39.160Z[UTC]").plusDays(7)
+  val BIGGER_EXPIRE_THAN_MAX: ZonedDateTime = 
ZonedDateTime.parse("2021-10-25T07:05:39.160Z[UTC]").plusDays(8)
+  val ALICE: Username = Username.of("alice")
+  val BOB: Username = Username.of("bob")
+}
+
+trait PushSubscriptionRepositoryContract {
+  def testee: PushSubscriptionRepository
+
+  // test for save API
+  @Test
+  def validSubscriptionShouldBeSavedSuccessfully(): Unit = {
+    val validRequest = PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId.apply("1"),
+      url = PushSubscriptionServerURL(new URL("https://example.com/push";)),
+      keys = Option.empty,
+      expires = Option.empty,
+      types = Seq(EmailTypeName)

Review comment:
       We can create our own types rather than import EmailTypeName that have 
little to do in `data-jmap`  package IMO

##########
File path: 
server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
##########
@@ -0,0 +1,163 @@
+/******************************************************************
+ * 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.jmap.memory.pushsubscription;
+
+import java.time.Clock;
+import java.time.ZonedDateTime;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.DeviceClientIdInvalidException;
+import org.apache.james.jmap.api.model.PushSubscription;
+import org.apache.james.jmap.api.model.PushSubscriptionCreationRequest;
+import org.apache.james.jmap.api.model.PushSubscriptionExpiredTime;
+import org.apache.james.jmap.api.model.PushSubscriptionId;
+import org.apache.james.jmap.api.model.PushSubscriptionNotFoundException;
+import org.apache.james.jmap.api.model.TypeName;
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository;
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionVerificationCodeFactory;
+import org.reactivestreams.Publisher;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import scala.jdk.javaapi.CollectionConverters;
+import scala.jdk.javaapi.OptionConverters;
+
+public class MemoryPushSubscriptionRepository implements 
PushSubscriptionRepository {
+    private final Table<Username, PushSubscriptionId, PushSubscription> table;
+    private final Clock clock;
+    private final PushSubscriptionVerificationCodeFactory 
verificationCodeFactory;
+
+    @Inject
+    public MemoryPushSubscriptionRepository(Clock clock, 
PushSubscriptionVerificationCodeFactory factory) {
+        this.clock = clock;
+        this.verificationCodeFactory = factory;
+        table = HashBasedTable.create();
+    }
+
+    @Override
+    public Publisher<PushSubscriptionId> save(Username username, 
PushSubscriptionCreationRequest request) {
+        if (request.expires().isDefined()) {
+            expireTimePreconditions(request.expires().get().value(), clock);

Review comment:
       Please return a Mono.error if it fails

##########
File path: 
server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/TypeName.scala
##########
@@ -0,0 +1,101 @@
+/******************************************************************
+ * 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.jmap.api.model
+
+trait TypeName {
+  def asMap(maybeState: Option[State]): Map[TypeName, State] =
+    maybeState.map(state => Map[TypeName, State](this -> state))
+      .getOrElse(Map())
+
+  def asString(): String
+  def parse(string: String): Option[TypeName]
+  def parseState(string: String): Either[IllegalArgumentException, State]
+}
+
+case object MailboxTypeName extends TypeName {

Review comment:
       Maybe we can keep the `trait TypeName` in data-jmap` but move all the 
case objects back to the RFC-8620 implementation?

##########
File path: 
server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepositoryTest.java
##########
@@ -0,0 +1,41 @@
+/******************************************************************
+ * 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.jmap.memory.pushsubscription;
+
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository;
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepositoryContract;
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionVerificationCodeFactory;
+import org.junit.jupiter.api.BeforeEach;
+
+public class MemoryPushSubscriptionRepositoryTest implements 
PushSubscriptionRepositoryContract {
+    PushSubscriptionRepository pushSubscriptionRepository;
+    PushSubscriptionVerificationCodeFactory verificationCodeFactory;
+
+    @BeforeEach
+    void setup() {
+        verificationCodeFactory = new 
PushSubscriptionVerificationCodeFactory.DefaultPushSubscriptionVerificationCodeFactory();

Review comment:
       Do we really need to be that generic? Do we have an example of a place 
we would not use the default one?

##########
File path: 
server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
##########
@@ -0,0 +1,87 @@
+/******************************************************************
+ * 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.jmap.api.model
+
+import java.net.URL
+import java.time.{Clock, ZonedDateTime}
+import java.util.UUID
+
+case class PushSubscriptionId(value: UUID)
+
+object DeviceClientId {
+  def asJavaString(deviceClientId: DeviceClientId): String = 
deviceClientId.value

Review comment:
       Can be a field of DeviceClientId case class

##########
File path: 
server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
##########
@@ -0,0 +1,163 @@
+/******************************************************************
+ * 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.jmap.memory.pushsubscription;
+
+import java.time.Clock;
+import java.time.ZonedDateTime;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.DeviceClientIdInvalidException;
+import org.apache.james.jmap.api.model.PushSubscription;
+import org.apache.james.jmap.api.model.PushSubscriptionCreationRequest;
+import org.apache.james.jmap.api.model.PushSubscriptionExpiredTime;
+import org.apache.james.jmap.api.model.PushSubscriptionId;
+import org.apache.james.jmap.api.model.PushSubscriptionNotFoundException;
+import org.apache.james.jmap.api.model.TypeName;
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository;
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionVerificationCodeFactory;
+import org.reactivestreams.Publisher;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import scala.jdk.javaapi.CollectionConverters;
+import scala.jdk.javaapi.OptionConverters;
+
+public class MemoryPushSubscriptionRepository implements 
PushSubscriptionRepository {
+    private final Table<Username, PushSubscriptionId, PushSubscription> table;
+    private final Clock clock;
+    private final PushSubscriptionVerificationCodeFactory 
verificationCodeFactory;
+
+    @Inject
+    public MemoryPushSubscriptionRepository(Clock clock, 
PushSubscriptionVerificationCodeFactory factory) {
+        this.clock = clock;
+        this.verificationCodeFactory = factory;
+        table = HashBasedTable.create();
+    }
+
+    @Override
+    public Publisher<PushSubscriptionId> save(Username username, 
PushSubscriptionCreationRequest request) {
+        if (request.expires().isDefined()) {
+            expireTimePreconditions(request.expires().get().value(), clock);
+        }
+        if (!isUniqueDeviceClientId(username, request.deviceClientId())) {
+            throw new DeviceClientIdInvalidException(request.deviceClientId(), 
"deviceClientId must be unique");
+        }
+        return Mono.fromCallable(() -> 
PushSubscriptionId.apply(UUID.randomUUID()))
+            .doOnNext(id -> table.put(username, id,
+                PushSubscription.from(request,
+                    id,
+                    
evaluateExpiresTime(OptionConverters.toJava(request.expires().map(PushSubscriptionExpiredTime::value)),
 clock),
+                    String.valueOf(verificationCodeFactory.generate()))));
+    }
+
+    @Override
+    public Publisher<Void> updateExpireTime(Username username, 
PushSubscriptionId id, ZonedDateTime newExpire) {
+        expireTimePreconditions(newExpire, clock);

Review comment:
       return Mono.error

##########
File path: 
server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
##########
@@ -0,0 +1,163 @@
+/******************************************************************
+ * 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.jmap.memory.pushsubscription;
+
+import java.time.Clock;
+import java.time.ZonedDateTime;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.DeviceClientIdInvalidException;
+import org.apache.james.jmap.api.model.PushSubscription;
+import org.apache.james.jmap.api.model.PushSubscriptionCreationRequest;
+import org.apache.james.jmap.api.model.PushSubscriptionExpiredTime;
+import org.apache.james.jmap.api.model.PushSubscriptionId;
+import org.apache.james.jmap.api.model.PushSubscriptionNotFoundException;
+import org.apache.james.jmap.api.model.TypeName;
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository;
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionVerificationCodeFactory;
+import org.reactivestreams.Publisher;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import scala.jdk.javaapi.CollectionConverters;
+import scala.jdk.javaapi.OptionConverters;
+
+public class MemoryPushSubscriptionRepository implements 
PushSubscriptionRepository {
+    private final Table<Username, PushSubscriptionId, PushSubscription> table;
+    private final Clock clock;
+    private final PushSubscriptionVerificationCodeFactory 
verificationCodeFactory;
+
+    @Inject
+    public MemoryPushSubscriptionRepository(Clock clock, 
PushSubscriptionVerificationCodeFactory factory) {
+        this.clock = clock;
+        this.verificationCodeFactory = factory;
+        table = HashBasedTable.create();
+    }
+
+    @Override
+    public Publisher<PushSubscriptionId> save(Username username, 
PushSubscriptionCreationRequest request) {
+        if (request.expires().isDefined()) {
+            expireTimePreconditions(request.expires().get().value(), clock);
+        }
+        if (!isUniqueDeviceClientId(username, request.deviceClientId())) {
+            throw new DeviceClientIdInvalidException(request.deviceClientId(), 
"deviceClientId must be unique");
+        }
+        return Mono.fromCallable(() -> 
PushSubscriptionId.apply(UUID.randomUUID()))

Review comment:
       Hide the internals of `PushSubscriptionId`: 
   
   ```suggestion
           return Mono.fromCallable(() -> PushSubscriptionId.generate())
   ```

##########
File path: 
server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
##########
@@ -0,0 +1,163 @@
+/******************************************************************
+ * 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.jmap.memory.pushsubscription;
+
+import java.time.Clock;
+import java.time.ZonedDateTime;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.DeviceClientIdInvalidException;
+import org.apache.james.jmap.api.model.PushSubscription;
+import org.apache.james.jmap.api.model.PushSubscriptionCreationRequest;
+import org.apache.james.jmap.api.model.PushSubscriptionExpiredTime;
+import org.apache.james.jmap.api.model.PushSubscriptionId;
+import org.apache.james.jmap.api.model.PushSubscriptionNotFoundException;
+import org.apache.james.jmap.api.model.TypeName;
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository;
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionVerificationCodeFactory;
+import org.reactivestreams.Publisher;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import scala.jdk.javaapi.CollectionConverters;
+import scala.jdk.javaapi.OptionConverters;
+
+public class MemoryPushSubscriptionRepository implements 
PushSubscriptionRepository {
+    private final Table<Username, PushSubscriptionId, PushSubscription> table;
+    private final Clock clock;
+    private final PushSubscriptionVerificationCodeFactory 
verificationCodeFactory;
+
+    @Inject
+    public MemoryPushSubscriptionRepository(Clock clock, 
PushSubscriptionVerificationCodeFactory factory) {
+        this.clock = clock;
+        this.verificationCodeFactory = factory;
+        table = HashBasedTable.create();
+    }
+
+    @Override
+    public Publisher<PushSubscriptionId> save(Username username, 
PushSubscriptionCreationRequest request) {
+        if (request.expires().isDefined()) {
+            expireTimePreconditions(request.expires().get().value(), clock);
+        }
+        if (!isUniqueDeviceClientId(username, request.deviceClientId())) {
+            throw new DeviceClientIdInvalidException(request.deviceClientId(), 
"deviceClientId must be unique");
+        }
+        return Mono.fromCallable(() -> 
PushSubscriptionId.apply(UUID.randomUUID()))
+            .doOnNext(id -> table.put(username, id,
+                PushSubscription.from(request,
+                    id,
+                    
evaluateExpiresTime(OptionConverters.toJava(request.expires().map(PushSubscriptionExpiredTime::value)),
 clock),
+                    String.valueOf(verificationCodeFactory.generate()))));
+    }
+
+    @Override
+    public Publisher<Void> updateExpireTime(Username username, 
PushSubscriptionId id, ZonedDateTime newExpire) {
+        expireTimePreconditions(newExpire, clock);
+        return Mono.justOrEmpty(table.get(username, id))
+            .doOnNext(pushSubscription -> {
+                PushSubscription newPushSubscription = PushSubscription.apply(
+                    pushSubscription.id(),
+                    pushSubscription.deviceClientId(),
+                    pushSubscription.url(),
+                    pushSubscription.keys(),
+                    pushSubscription.verificationCode(),
+                    pushSubscription.validated(),
+                    
PushSubscriptionExpiredTime.apply(evaluateExpiresTime(Optional.of(newExpire), 
clock)),
+                    pushSubscription.types());
+                table.put(username, id, newPushSubscription);
+            })
+            .switchIfEmpty(Mono.error(() -> new 
PushSubscriptionNotFoundException(id)))
+            .then();
+    }
+
+    @Override
+    public Publisher<Void> updateTypes(Username username, PushSubscriptionId 
id, Set<TypeName> types) {
+        return Mono.justOrEmpty(table.get(username, id))
+            .doOnNext(pushSubscription -> {
+                PushSubscription newPushSubscription = PushSubscription.apply(
+                    pushSubscription.id(),
+                    pushSubscription.deviceClientId(),
+                    pushSubscription.url(),
+                    pushSubscription.keys(),
+                    pushSubscription.verificationCode(),
+                    pushSubscription.validated(),
+                    pushSubscription.expires(),
+                    CollectionConverters.asScala(types).toSeq());
+                table.put(username, id, newPushSubscription);

Review comment:
       pushSubscription.withTypes ?

##########
File path: 
server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushSubscriptionRepositoryContract.scala
##########
@@ -0,0 +1,174 @@
+/** ****************************************************************
+ * 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.jmap.api.pushsubscription
+
+import java.net.URL
+import java.time.{Clock, Instant, ZoneId, ZonedDateTime}
+
+import org.apache.james.core.Username
+import org.apache.james.jmap.api.model.{DeviceClientId, 
DeviceClientIdInvalidException, EmailTypeName, ExpireTimeInvalidException, 
PushSubscriptionCreationRequest, PushSubscriptionExpiredTime, 
PushSubscriptionServerURL}
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepositoryContract.{ALICE,
 BIGGER_EXPIRE_THAN_MAX, INVALID_EXPIRE, MAX_EXPIRE}
+import org.assertj.core.api.Assertions.{assertThat, assertThatThrownBy}
+import org.junit.jupiter.api.Test
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+import scala.jdk.CollectionConverters._
+
+object PushSubscriptionRepositoryContract {
+  val NOW: Instant = Instant.parse("2021-10-25T07:05:39.160Z")
+  val CLOCK: Clock = Clock.fixed(NOW, ZoneId.of("UTC"))
+  val INVALID_EXPIRE: ZonedDateTime = 
ZonedDateTime.parse("2020-10-25T07:05:39.160Z[UTC]")
+  val MAX_EXPIRE: ZonedDateTime = 
ZonedDateTime.parse("2021-10-25T07:05:39.160Z[UTC]").plusDays(7)
+  val BIGGER_EXPIRE_THAN_MAX: ZonedDateTime = 
ZonedDateTime.parse("2021-10-25T07:05:39.160Z[UTC]").plusDays(8)
+  val ALICE: Username = Username.of("alice")
+  val BOB: Username = Username.of("bob")
+}
+
+trait PushSubscriptionRepositoryContract {
+  def testee: PushSubscriptionRepository
+
+  // test for save API
+  @Test
+  def validSubscriptionShouldBeSavedSuccessfully(): Unit = {
+    val validRequest = PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId.apply("1"),
+      url = PushSubscriptionServerURL(new URL("https://example.com/push";)),
+      keys = Option.empty,
+      expires = Option.empty,
+      types = Seq(EmailTypeName)
+    )
+    val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block()
+    val singleRecordSaved = SFlux.fromPublisher(testee.get(ALICE, 
Set(pushSubscriptionId).asJava)).count().block()
+
+    assertThat(singleRecordSaved).isEqualTo(1);
+  }
+
+  @Test
+  def newSavedSubscriptionShouldNotBeValidated(): Unit = {
+    val validRequest = PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId.apply("1"),
+      url = PushSubscriptionServerURL(new URL("https://example.com/push";)),
+      keys = Option.empty,
+      expires = Option.empty,
+      types = Seq(EmailTypeName)
+    )
+    val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block()
+    val newSavedSubscription = SFlux.fromPublisher(testee.get(ALICE, 
Set(pushSubscriptionId).asJava)).blockFirst().get
+
+    assertThat(newSavedSubscription.validated).isEqualTo(false)
+  }
+
+  @Test
+  def subscriptionWithExpireBiggerThanMaxExpireShouldBeSetToMaxExpire(): Unit 
= {
+    val request = PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId.apply("1"),
+      url = PushSubscriptionServerURL(new URL("https://example.com/push";)),
+      keys = Option.empty,
+      expires = 
Option.apply(PushSubscriptionExpiredTime(BIGGER_EXPIRE_THAN_MAX)),
+      types = Seq(EmailTypeName)
+    )
+    val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
request)).block()
+    val newSavedSubscription = SFlux.fromPublisher(testee.get(ALICE, 
Set(pushSubscriptionId).asJava)).blockFirst().get
+
+    assertThat(newSavedSubscription.expires.value).isEqualTo(MAX_EXPIRE)
+  }
+
+  @Test
+  def subscriptionWithInvalidExpireTimeShouldThrowException(): Unit = {
+    val invalidRequest = PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId.apply("1"),
+      url = PushSubscriptionServerURL(new URL("https://example.com/push";)),
+      keys = Option.empty,
+      expires = Option.apply(PushSubscriptionExpiredTime(INVALID_EXPIRE)),
+      types = Seq(EmailTypeName)
+    )
+    assertThatThrownBy(() => SMono.fromPublisher(testee.save(ALICE, 
invalidRequest)).block())
+      .isExactlyInstanceOf(classOf[ExpireTimeInvalidException])
+  }
+
+  @Test
+  def subscriptionWithDuplicatedDeviceClientIdShouldThrowException(): Unit = {
+    val firstRequest = PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId.apply("1"),
+      url = PushSubscriptionServerURL(new URL("https://example.com/push";)),
+      keys = Option.empty,
+      expires = Option.empty,
+      types = Seq(EmailTypeName)
+    )
+    SMono.fromPublisher(testee.save(ALICE, firstRequest)).block()
+
+    val secondRequestWithDuplicatedDeviceClientId = 
PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId.apply("1"),
+      url = PushSubscriptionServerURL(new URL("https://example.com/push";)),
+      keys = Option.empty,
+      expires = Option.empty,
+      types = Seq(EmailTypeName)
+    )
+
+    assertThatThrownBy(() => SMono.fromPublisher(testee.save(ALICE, 
secondRequestWithDuplicatedDeviceClientId)).block())
+      .isExactlyInstanceOf(classOf[DeviceClientIdInvalidException])
+  }
+
+  // todo test for updateExpireTime API
+//  - expire < now -> throw ex
+//  - expire > max -> set expire = max
+//  - update with PId not found -> throw ex
+//  - valid expire -> update successfully
+
+  // todo test for updateTypes API
+//  - update successfully
+//  - update with PId not found -> throw ex
+
+  // todo test for revoke API
+  @Test
+  def revokeStoredSubscriptionShouldSuccessfully(): Unit = {
+    // store a subscription
+    val validRequest = PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId.apply("1"),
+      url = PushSubscriptionServerURL(new URL("https://example.com/push";)),
+      keys = Option.empty,
+      expires = Option.empty,
+      types = Seq(EmailTypeName)
+    )
+    val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block()
+    val singleRecordSaved = SFlux.fromPublisher(testee.get(ALICE, 
Set(pushSubscriptionId).asJava)).count().block()
+    assertThat(singleRecordSaved).isEqualTo(1)
+
+    // revoke that subscription
+    SMono.fromPublisher(testee.revoke(ALICE, pushSubscriptionId)).block()
+    val remainRecordNumber = SFlux.fromPublisher(testee.get(ALICE, 
Set(pushSubscriptionId).asJava)).count().block()
+
+    assertThat(remainRecordNumber).isEqualTo(0)
+  }
+  // todo test for get API
+//  - succeed case
+//  - should not return outdated subscriptions
+//  - PId not found -> throw ex
+
+  // todo test for list API
+//  - succeed case(2-3 subscriptions)
+//  - should not return outdated subscriptions
+
+  // todo test for validateVerificationCode API
+//  - Pid not found -> throw ex
+//  - succeed case: validated -> true

Review comment:
       Please do not forget to solve these comments and remove them.

##########
File path: 
server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
##########
@@ -0,0 +1,163 @@
+/******************************************************************
+ * 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.jmap.memory.pushsubscription;
+
+import java.time.Clock;
+import java.time.ZonedDateTime;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.DeviceClientIdInvalidException;
+import org.apache.james.jmap.api.model.PushSubscription;
+import org.apache.james.jmap.api.model.PushSubscriptionCreationRequest;
+import org.apache.james.jmap.api.model.PushSubscriptionExpiredTime;
+import org.apache.james.jmap.api.model.PushSubscriptionId;
+import org.apache.james.jmap.api.model.PushSubscriptionNotFoundException;
+import org.apache.james.jmap.api.model.TypeName;
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository;
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionVerificationCodeFactory;
+import org.reactivestreams.Publisher;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import scala.jdk.javaapi.CollectionConverters;
+import scala.jdk.javaapi.OptionConverters;
+
+public class MemoryPushSubscriptionRepository implements 
PushSubscriptionRepository {
+    private final Table<Username, PushSubscriptionId, PushSubscription> table;
+    private final Clock clock;
+    private final PushSubscriptionVerificationCodeFactory 
verificationCodeFactory;
+
+    @Inject
+    public MemoryPushSubscriptionRepository(Clock clock, 
PushSubscriptionVerificationCodeFactory factory) {
+        this.clock = clock;
+        this.verificationCodeFactory = factory;
+        table = HashBasedTable.create();
+    }
+
+    @Override
+    public Publisher<PushSubscriptionId> save(Username username, 
PushSubscriptionCreationRequest request) {
+        if (request.expires().isDefined()) {
+            expireTimePreconditions(request.expires().get().value(), clock);
+        }
+        if (!isUniqueDeviceClientId(username, request.deviceClientId())) {
+            throw new DeviceClientIdInvalidException(request.deviceClientId(), 
"deviceClientId must be unique");
+        }
+        return Mono.fromCallable(() -> 
PushSubscriptionId.apply(UUID.randomUUID()))
+            .doOnNext(id -> table.put(username, id,
+                PushSubscription.from(request,
+                    id,
+                    
evaluateExpiresTime(OptionConverters.toJava(request.expires().map(PushSubscriptionExpiredTime::value)),
 clock),
+                    String.valueOf(verificationCodeFactory.generate()))));
+    }
+
+    @Override
+    public Publisher<Void> updateExpireTime(Username username, 
PushSubscriptionId id, ZonedDateTime newExpire) {
+        expireTimePreconditions(newExpire, clock);
+        return Mono.justOrEmpty(table.get(username, id))
+            .doOnNext(pushSubscription -> {
+                PushSubscription newPushSubscription = PushSubscription.apply(
+                    pushSubscription.id(),
+                    pushSubscription.deviceClientId(),
+                    pushSubscription.url(),
+                    pushSubscription.keys(),
+                    pushSubscription.verificationCode(),
+                    pushSubscription.validated(),
+                    
PushSubscriptionExpiredTime.apply(evaluateExpiresTime(Optional.of(newExpire), 
clock)),
+                    pushSubscription.types());

Review comment:
       BTW on Scala side we may be able to write
   
   ```
   case class PushSubscription(...) {
     // ...
     
     def withExpireDate(e: ExpireDate): PushSubscription = copy(expireDate = 
expireDate)
   }
   ```
   
   A beautiful one liner <3
   
   https://alvinalexander.com/scala/scala-case-class-copy-method-example/

##########
File path: 
server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
##########
@@ -0,0 +1,163 @@
+/******************************************************************
+ * 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.jmap.memory.pushsubscription;
+
+import java.time.Clock;
+import java.time.ZonedDateTime;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.DeviceClientIdInvalidException;
+import org.apache.james.jmap.api.model.PushSubscription;
+import org.apache.james.jmap.api.model.PushSubscriptionCreationRequest;
+import org.apache.james.jmap.api.model.PushSubscriptionExpiredTime;
+import org.apache.james.jmap.api.model.PushSubscriptionId;
+import org.apache.james.jmap.api.model.PushSubscriptionNotFoundException;
+import org.apache.james.jmap.api.model.TypeName;
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository;
+import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionVerificationCodeFactory;
+import org.reactivestreams.Publisher;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import scala.jdk.javaapi.CollectionConverters;
+import scala.jdk.javaapi.OptionConverters;
+
+public class MemoryPushSubscriptionRepository implements 
PushSubscriptionRepository {
+    private final Table<Username, PushSubscriptionId, PushSubscription> table;
+    private final Clock clock;
+    private final PushSubscriptionVerificationCodeFactory 
verificationCodeFactory;
+
+    @Inject
+    public MemoryPushSubscriptionRepository(Clock clock, 
PushSubscriptionVerificationCodeFactory factory) {
+        this.clock = clock;
+        this.verificationCodeFactory = factory;
+        table = HashBasedTable.create();
+    }
+
+    @Override
+    public Publisher<PushSubscriptionId> save(Username username, 
PushSubscriptionCreationRequest request) {
+        if (request.expires().isDefined()) {
+            expireTimePreconditions(request.expires().get().value(), clock);
+        }
+        if (!isUniqueDeviceClientId(username, request.deviceClientId())) {
+            throw new DeviceClientIdInvalidException(request.deviceClientId(), 
"deviceClientId must be unique");
+        }
+        return Mono.fromCallable(() -> 
PushSubscriptionId.apply(UUID.randomUUID()))
+            .doOnNext(id -> table.put(username, id,
+                PushSubscription.from(request,
+                    id,
+                    
evaluateExpiresTime(OptionConverters.toJava(request.expires().map(PushSubscriptionExpiredTime::value)),
 clock),
+                    String.valueOf(verificationCodeFactory.generate()))));
+    }
+
+    @Override
+    public Publisher<Void> updateExpireTime(Username username, 
PushSubscriptionId id, ZonedDateTime newExpire) {
+        expireTimePreconditions(newExpire, clock);
+        return Mono.justOrEmpty(table.get(username, id))
+            .doOnNext(pushSubscription -> {
+                PushSubscription newPushSubscription = PushSubscription.apply(
+                    pushSubscription.id(),
+                    pushSubscription.deviceClientId(),
+                    pushSubscription.url(),
+                    pushSubscription.keys(),
+                    pushSubscription.verificationCode(),
+                    pushSubscription.validated(),
+                    
PushSubscriptionExpiredTime.apply(evaluateExpiresTime(Optional.of(newExpire), 
clock)),
+                    pushSubscription.types());
+                table.put(username, id, newPushSubscription);
+            })
+            .switchIfEmpty(Mono.error(() -> new 
PushSubscriptionNotFoundException(id)))
+            .then();
+    }
+
+    @Override
+    public Publisher<Void> updateTypes(Username username, PushSubscriptionId 
id, Set<TypeName> types) {
+        return Mono.justOrEmpty(table.get(username, id))
+            .doOnNext(pushSubscription -> {
+                PushSubscription newPushSubscription = PushSubscription.apply(
+                    pushSubscription.id(),
+                    pushSubscription.deviceClientId(),
+                    pushSubscription.url(),
+                    pushSubscription.keys(),
+                    pushSubscription.verificationCode(),
+                    pushSubscription.validated(),
+                    pushSubscription.expires(),
+                    CollectionConverters.asScala(types).toSeq());
+                table.put(username, id, newPushSubscription);
+            })
+            .switchIfEmpty(Mono.error(() -> new 
PushSubscriptionNotFoundException(id)))
+            .then();
+    }
+
+    @Override
+    public Publisher<Void> revoke(Username username, PushSubscriptionId id) {
+        return Mono.fromCallable(() -> table.remove(username, id)).then();
+    }
+
+    @Override
+    public Publisher<PushSubscription> get(Username username, 
Set<PushSubscriptionId> ids) {
+        return Flux.fromStream(table.row(username).entrySet().stream())
+            .filter(entry -> ids.contains(entry.getKey()))
+            .map(Map.Entry::getValue)
+            .filter(subscription -> isNotOutdatedSubscription(subscription, 
clock));
+    }
+
+    @Override
+    public Publisher<PushSubscription> list(Username username) {
+        return Flux.fromStream(table.row(username).entrySet().stream())
+            .map(Map.Entry::getValue)
+            .filter(subscription -> isNotOutdatedSubscription(subscription, 
clock));
+    }
+
+    @Override
+    public Publisher<Void> validateVerificationCode(Username username, 
PushSubscriptionId id) {
+        return Mono.justOrEmpty(table.get(username, id))
+            .doOnNext(pushSubscription -> {
+                if (!pushSubscription.validated()) {
+                    PushSubscription newPushSubscription = 
PushSubscription.apply(
+                        pushSubscription.id(),
+                        pushSubscription.deviceClientId(),
+                        pushSubscription.url(),
+                        pushSubscription.keys(),
+                        pushSubscription.verificationCode(),
+                        true,
+                        pushSubscription.expires(),
+                        pushSubscription.types());

Review comment:
       pushSubscription.verified ?

##########
File path: 
server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
##########
@@ -0,0 +1,87 @@
+/******************************************************************
+ * 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.jmap.api.model
+
+import java.net.URL
+import java.time.{Clock, ZonedDateTime}
+import java.util.UUID
+
+case class PushSubscriptionId(value: UUID)
+
+object DeviceClientId {
+  def asJavaString(deviceClientId: DeviceClientId): String = 
deviceClientId.value

Review comment:
       BTW a java client can call `deviceClientId.value()`?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to