From 75df1427edad1a7be9650c92df04dfae1e8c9647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 18 May 2018 14:37:49 +0200 Subject: [PATCH 01/41] Add admin API to docs --- README.md | 4 ++++ admin-api/{API.md => README.md} | 0 client-api/{API.md => README.md} | 0 3 files changed, 4 insertions(+) rename admin-api/{API.md => README.md} (100%) rename client-api/{API.md => README.md} (100%) diff --git a/README.md b/README.md index bf6a3c2cb..2a2a28433 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,12 @@ Mono Repository for core protocols and services around a OCS/BSS for packet data [analytics](./analytics/README.md) +[admin-api](./admin-api/README.md) + [auth-server](./auth-server/README.md) +[client-api](./client-api/README.md) + [diameter-stack](./diameter-stack/README.md) [diameter-test](./diameter-test/README.md) diff --git a/admin-api/API.md b/admin-api/README.md similarity index 100% rename from admin-api/API.md rename to admin-api/README.md diff --git a/client-api/API.md b/client-api/README.md similarity index 100% rename from client-api/API.md rename to client-api/README.md From 20ae5b3417c5ba51ec5429665766989ac44bb0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 18 May 2018 14:42:58 +0200 Subject: [PATCH 02/41] Add more details to the client-api README, pointing to swagger and the swagger-generated docs. --- client-api/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client-api/README.md b/client-api/README.md index fd4709029..507ba158c 100644 --- a/client-api/README.md +++ b/client-api/README.md @@ -53,6 +53,10 @@ Furthermore: - All client interactions goes through the backend, including handling of authentication, payment etc. - Subscriptions as such has already been activated through the CRM system including registration of email address etc. + +The API is developed partly through this document. Partly through the swagger specification of the +prime/infra/prime-api.yaml file that is more or less reliably mirrored in the swagger-generated static website [swagger doc](https://ostelco.github.io/). +The specs are a bit in flux right now, but we expect it to settle down over the coming few weeks. ## Data model From 9edd66c8688f7cb2c6cf641dae185185a30b10f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Sat, 19 May 2018 15:25:12 +0200 Subject: [PATCH 03/41] More about how to find directories --- docs/TEST.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/TEST.md b/docs/TEST.md index 9b5f5d579..285339191 100644 --- a/docs/TEST.md +++ b/docs/TEST.md @@ -4,7 +4,10 @@ * Configure firebase project - `pantel-tests` or `pantel-2decb` - * Save `pantel-prod.json` in all folders where this file is added in `.gitignore`. + * Save `pantel-prod.json` in all folders where this file is added in `.gitignore`. You can find these directories by + executing the command: + + grep -i pantel $(find . -name '.gitignore') | awk -F: '{print $1}' | sort | uniq | sed 's/.gitignore//g' * Create test subscriber From 9f8ce196e1abcbc0b344c78eb897157e1eadfe32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Sat, 19 May 2018 15:26:36 +0200 Subject: [PATCH 04/41] Ignore pantel-prod.json --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 91c39443c..f552c1c1e 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ out target secrets/* .nb-gradle +prime/src/integration-tests/resources/pantel-prod.json From a878b085d6d79516bb9e8173f5324a299462843d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 25 May 2018 08:44:11 +0200 Subject: [PATCH 05/41] Add comment, introduce constants and enable TSL --- .../ocsgw/data/grpc/GrpcDataSource.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/data/grpc/GrpcDataSource.java b/ocsgw/src/main/java/org/ostelco/ocsgw/data/grpc/GrpcDataSource.java index 92f7e6fa9..4bc09b657 100644 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/data/grpc/GrpcDataSource.java +++ b/ocsgw/src/main/java/org/ostelco/ocsgw/data/grpc/GrpcDataSource.java @@ -5,6 +5,8 @@ import io.grpc.ManagedChannelBuilder; import io.grpc.StatusRuntimeException; import io.grpc.auth.MoreCallCredentials; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyChannelBuilder; import io.grpc.stub.StreamObserver; import org.jdiameter.api.IllegalDiameterStateException; import org.jdiameter.api.InternalException; @@ -28,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; @@ -51,6 +54,10 @@ public class GrpcDataSource implements DataSource { private static final Logger LOG = LoggerFactory.getLogger(GrpcDataSource.class); + public static final int KEEP_ALIVE_TIMEOUT_IN_MINUTES = 1; + + public static final int KEEP_ALIVE_TIME_IN_SECONDS = 50; + private final OcsServiceGrpc.OcsServiceStub ocsServiceStub; private final Set blocked = new HashSet<>(); @@ -142,6 +149,15 @@ private void reconnectCreditControlRequest() { } } + /** + * + * Generate a new instande that connects to an endpoint, and + * optionally also encrypts the connection. + * + * @param target The gRPC endpoint to connect the client to. + * @param encrypted True iff transport level encryption is enabled. + * @throws IOException + */ public GrpcDataSource(final String target, final boolean encrypted) throws IOException { LOG.info("Created GrpcDataSource"); @@ -149,20 +165,22 @@ public GrpcDataSource(final String target, final boolean encrypted) throws IOExc LOG.info("encrypted : {}", encrypted); // Set up a channel to be used to communicate as an OCS instance, // to a gRPC instance. + final ManagedChannelBuilder channelBuilder = ManagedChannelBuilder .forTarget(target) .keepAliveWithoutCalls(true) - .keepAliveTimeout(1, TimeUnit.MINUTES) - .keepAliveTime(50, TimeUnit.SECONDS); + .keepAliveTimeout(KEEP_ALIVE_TIMEOUT_IN_MINUTES, TimeUnit.MINUTES) + .keepAliveTime(KEEP_ALIVE_TIME_IN_SECONDS, TimeUnit.SECONDS); // Initialize the stub that will be used to actually // communicate from the client emulating being the OCS. if (encrypted) { + final String serviceAccountFile = System.getenv("GOOGLE_APPLICATION_CREDENTIALS"); final ServiceAccountJwtAccessCredentials credentials = ServiceAccountJwtAccessCredentials.fromStream(new FileInputStream(serviceAccountFile)); final ManagedChannel channel = channelBuilder - .usePlaintext(true) // FIXME enable TLS and then remove this + .useTransportSecurity() .build(); ocsServiceStub = OcsServiceGrpc.newStub(channel) .withCallCredentials(MoreCallCredentials.from(credentials)); From f2bba7e8310b1a9365f7d2ad407edac95f4a4e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 25 May 2018 08:44:32 +0200 Subject: [PATCH 06/41] Enable SSL certificate for gRPC interface --- prime/infra/prime.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/prime/infra/prime.yaml b/prime/infra/prime.yaml index 3dd08da1b..94b95e3d0 100644 --- a/prime/infra/prime.yaml +++ b/prime/infra/prime.yaml @@ -65,6 +65,10 @@ spec: ] ports: - containerPort: 9000 + volumeMounts: + - mountPath: /etc/nginx/ssl + name: ocs-ostelco-ssl + readOnly: true - name: api-esp image: gcr.io/endpoints-release/endpoints-runtime:1 args: [ @@ -99,3 +103,6 @@ spec: - name: api-ostelco-ssl secret: secretName: api-ostelco-ssl + - name: ocs-ostelco-ssl + secret: + secretName: ocs-ostelco-ssl From e85bbc3c8f7c9110a1678a42ec2ba5494debb701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 25 May 2018 08:44:11 +0200 Subject: [PATCH 07/41] Add comment, introduce constants and enable TSL --- .../ocsgw/data/grpc/GrpcDataSource.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/data/grpc/GrpcDataSource.java b/ocsgw/src/main/java/org/ostelco/ocsgw/data/grpc/GrpcDataSource.java index 92f7e6fa9..4bc09b657 100644 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/data/grpc/GrpcDataSource.java +++ b/ocsgw/src/main/java/org/ostelco/ocsgw/data/grpc/GrpcDataSource.java @@ -5,6 +5,8 @@ import io.grpc.ManagedChannelBuilder; import io.grpc.StatusRuntimeException; import io.grpc.auth.MoreCallCredentials; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyChannelBuilder; import io.grpc.stub.StreamObserver; import org.jdiameter.api.IllegalDiameterStateException; import org.jdiameter.api.InternalException; @@ -28,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; @@ -51,6 +54,10 @@ public class GrpcDataSource implements DataSource { private static final Logger LOG = LoggerFactory.getLogger(GrpcDataSource.class); + public static final int KEEP_ALIVE_TIMEOUT_IN_MINUTES = 1; + + public static final int KEEP_ALIVE_TIME_IN_SECONDS = 50; + private final OcsServiceGrpc.OcsServiceStub ocsServiceStub; private final Set blocked = new HashSet<>(); @@ -142,6 +149,15 @@ private void reconnectCreditControlRequest() { } } + /** + * + * Generate a new instande that connects to an endpoint, and + * optionally also encrypts the connection. + * + * @param target The gRPC endpoint to connect the client to. + * @param encrypted True iff transport level encryption is enabled. + * @throws IOException + */ public GrpcDataSource(final String target, final boolean encrypted) throws IOException { LOG.info("Created GrpcDataSource"); @@ -149,20 +165,22 @@ public GrpcDataSource(final String target, final boolean encrypted) throws IOExc LOG.info("encrypted : {}", encrypted); // Set up a channel to be used to communicate as an OCS instance, // to a gRPC instance. + final ManagedChannelBuilder channelBuilder = ManagedChannelBuilder .forTarget(target) .keepAliveWithoutCalls(true) - .keepAliveTimeout(1, TimeUnit.MINUTES) - .keepAliveTime(50, TimeUnit.SECONDS); + .keepAliveTimeout(KEEP_ALIVE_TIMEOUT_IN_MINUTES, TimeUnit.MINUTES) + .keepAliveTime(KEEP_ALIVE_TIME_IN_SECONDS, TimeUnit.SECONDS); // Initialize the stub that will be used to actually // communicate from the client emulating being the OCS. if (encrypted) { + final String serviceAccountFile = System.getenv("GOOGLE_APPLICATION_CREDENTIALS"); final ServiceAccountJwtAccessCredentials credentials = ServiceAccountJwtAccessCredentials.fromStream(new FileInputStream(serviceAccountFile)); final ManagedChannel channel = channelBuilder - .usePlaintext(true) // FIXME enable TLS and then remove this + .useTransportSecurity() .build(); ocsServiceStub = OcsServiceGrpc.newStub(channel) .withCallCredentials(MoreCallCredentials.from(credentials)); From 0eadeaef23d295562afebc3428ee5a1ee05a0e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Remseth?= Date: Fri, 25 May 2018 08:44:32 +0200 Subject: [PATCH 08/41] Enable SSL certificate for gRPC interface --- prime/infra/prime.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/prime/infra/prime.yaml b/prime/infra/prime.yaml index 311c73a3e..41f6b515d 100644 --- a/prime/infra/prime.yaml +++ b/prime/infra/prime.yaml @@ -65,6 +65,10 @@ spec: ] ports: - containerPort: 9000 + volumeMounts: + - mountPath: /etc/nginx/ssl + name: ocs-ostelco-ssl + readOnly: true - name: api-esp image: gcr.io/endpoints-release/endpoints-runtime:1 args: [ @@ -99,3 +103,6 @@ spec: - name: api-ostelco-ssl secret: secretName: api-ostelco-ssl + - name: ocs-ostelco-ssl + secret: + secretName: ocs-ostelco-ssl From e72e6e91220b96ad50af6c27675233cbd8136a3a Mon Sep 17 00:00:00 2001 From: Martin Cederlof Date: Fri, 8 Jun 2018 13:06:45 +0200 Subject: [PATCH 09/41] first cut at applicationToken API --- .../ostelco/prime/client/api/TopupModule.kt | 7 +- .../api/resources/ApplicationTokenResource.kt | 44 +++++++++ .../resources/ApplicationTokenResourceTest.kt | 91 +++++++++++++++++++ .../org/ostelco/prime/model/Entities.kt | 5 + prime/infra/prime-client-api.yaml | 36 ++++++++ 5 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt create mode 100644 client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/TopupModule.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/TopupModule.kt index dc3c28384..90ee03f1e 100644 --- a/client-api/src/main/kotlin/org/ostelco/prime/client/api/TopupModule.kt +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/TopupModule.kt @@ -13,11 +13,7 @@ import io.dropwizard.client.JerseyClientBuilder import io.dropwizard.setup.Environment import org.ostelco.prime.client.api.auth.AccessTokenPrincipal import org.ostelco.prime.client.api.auth.OAuthAuthenticator -import org.ostelco.prime.client.api.resources.AnalyticsResource -import org.ostelco.prime.client.api.resources.ConsentsResource -import org.ostelco.prime.client.api.resources.ProductsResource -import org.ostelco.prime.client.api.resources.ProfileResource -import org.ostelco.prime.client.api.resources.SubscriptionResource +import org.ostelco.prime.client.api.resources.* import org.ostelco.prime.client.api.store.SubscriberDAOImpl import org.ostelco.prime.logger import org.ostelco.prime.module.PrimeModule @@ -57,6 +53,7 @@ class TopupModule : PrimeModule { jerseyEnv.register(ProductsResource(dao)) jerseyEnv.register(ProfileResource(dao)) jerseyEnv.register(SubscriptionResource(dao, client, config.pseudonymEndpoint!!)) + jerseyEnv.register(ApplicationTokenResource(dao)) /* For reporting OAuth2 caching events. */ val metrics = SharedMetricRegistries.getOrCreate(env.getName()) diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt new file mode 100644 index 000000000..6d9315977 --- /dev/null +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt @@ -0,0 +1,44 @@ +package org.ostelco.prime.client.api.resources + +import io.dropwizard.auth.Auth +import org.ostelco.prime.client.api.auth.AccessTokenPrincipal +import org.ostelco.prime.client.api.store.SubscriberDAO +import org.ostelco.prime.model.ApplicationToken +import javax.validation.constraints.NotNull +import javax.ws.rs.* +import javax.ws.rs.core.Response + +/** + * ApplicationToken API. + * + */ +@Path("/applicationtoken") +class ApplicationTokenResource(private val dao: SubscriberDAO) : ResourceHelpers() { + + @POST + @Produces("application/json") + @Consumes("application/json") + fun storeApplicationToken(@Auth authToken: AccessTokenPrincipal?, + @NotNull applicationToken: ApplicationToken): Response { + if (authToken == null) { + return Response.status(Response.Status.UNAUTHORIZED) + .build() + } + + val msisdn = dao.getMsisdn(authToken.name) + + println("ApplicationTokenResource called with msisdn : $msisdn and applicationToken : $applicationToken") + + val result = dao.getSubscriptionStatus(authToken.name) + + return if (result.isRight) { + Response.status(Response.Status.CREATED) + .entity(asJson(result.right().get())) + .build() + } else { + Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(asJson(result.left().get())) + .build() + } + } +} diff --git a/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt b/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt new file mode 100644 index 000000000..5321ceb18 --- /dev/null +++ b/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt @@ -0,0 +1,91 @@ +package org.ostelco.prime.client.api.resources + +import com.nhaarman.mockito_kotlin.argumentCaptor +import io.dropwizard.auth.AuthDynamicFeature +import io.dropwizard.auth.AuthValueFactoryProvider +import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter +import io.dropwizard.testing.junit.ResourceTestRule +import io.vavr.control.Either +import org.assertj.core.api.Assertions.assertThat +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory +import org.junit.Before +import org.junit.ClassRule +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import org.ostelco.prime.client.api.auth.AccessTokenPrincipal +import org.ostelco.prime.client.api.auth.OAuthAuthenticator +import org.ostelco.prime.client.api.core.ApiError +import org.ostelco.prime.client.api.store.SubscriberDAO +import org.ostelco.prime.client.api.util.AccessToken +import org.ostelco.prime.model.ApplicationToken +import org.ostelco.prime.model.Subscriber +import java.util.* +import javax.ws.rs.client.Entity +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response + +/** + * ApplicationToken API tests. + * + */ +class ApplicationTokenResourceTest { + + private val email = "boaty@internet.org" + + private val token = "testToken:kshfkajhka" + private val applicationID = "myAppID:4378932" + private val tokenType = "FCM" + + @Before + @Throws(Exception::class) + fun setUp() { + `when`(AUTHENTICATOR.authenticate(ArgumentMatchers.anyString())) + .thenReturn(Optional.of(AccessTokenPrincipal(email))) + } + + @Test + @Throws(Exception::class) + fun storeApplicationToken() { + val arg1 = argumentCaptor() + val arg2 = argumentCaptor() + + /* + `when`(DAO.createProfile(arg1.capture(), arg2.capture())) + .thenReturn(Either.right(profile)) + */ + val resp = RULE.target("/applicationtoken") + .request(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer ${AccessToken.withEmail(email)}") + .post(Entity.json("{\n" + + " \"token\": \"" + token + "\",\n" + + " \"applicationID\": \"" + applicationID + "\",\n" + + " \"tokenType\": \"" + tokenType + "\",\n" + + "}\n")) + + assertThat(resp.status).isEqualTo(Response.Status.CREATED.statusCode) + assertThat(resp.mediaType.toString()).isEqualTo(MediaType.APPLICATION_JSON) + assertThat(arg1.firstValue).isEqualTo(email) + } + + companion object { + + val DAO = mock(SubscriberDAO::class.java) + val AUTHENTICATOR = mock(OAuthAuthenticator::class.java) + + @JvmField + @ClassRule + val RULE = ResourceTestRule.builder() + .addResource(AuthDynamicFeature( + OAuthCredentialAuthFilter.Builder() + .setAuthenticator(AUTHENTICATOR) + .setPrefix("Bearer") + .buildAuthFilter())) + .addResource(AuthValueFactoryProvider.Binder(AccessTokenPrincipal::class.java)) + .addResource(ProfileResource(DAO)) + .setTestContainerFactory(GrizzlyWebTestContainerFactory()) + .build() + } +} diff --git a/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt b/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt index 0aafca6be..703adaa9d 100644 --- a/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt +++ b/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt @@ -38,6 +38,11 @@ data class Subscriber( } } +data class ApplicationToken( + var token: String = "", + var applicationID: String = "", + var tokenType: String = "") + data class Price( var amount: Int = 0, var currency: String = "") diff --git a/prime/infra/prime-client-api.yaml b/prime/infra/prime-client-api.yaml index f83cea312..3c4f0bbc6 100644 --- a/prime/infra/prime-client-api.yaml +++ b/prime/infra/prime-client-api.yaml @@ -64,6 +64,27 @@ paths: description: "Profile not found." security: - auth0_jwt: [] + "/applicationtoken": + post: + description: "Store application token" + consumes: + - application/json + produces: + - application/json + operationId: "storeApplicationToken" + parameters: + - name: applicationToken + in: body + description: application token + schema: + $ref: '#/definitions/ApplicationToken' + responses: + 201: + description: "Successfully stored token." + 500: + description: "Not able to store token." + security: + - auth0_jwt: [] "/products": get: description: "Get all products for the user." @@ -262,6 +283,21 @@ definitions: required: - amount - currency + ApplicationToken: + type: object + properties: + token: + description: "Application token" + type: string + applicationID: + description: "Uniquely identifier for the app instance" + type: string + tokenType: + description: "Type of application token (FCM)" + type: string + required: + - token + - applicationID PseudonymEntity: type: object properties: From c9d700ffc77364259fbc62072b81e77bab7f8d27 Mon Sep 17 00:00:00 2001 From: Martin Cederlof Date: Fri, 8 Jun 2018 14:02:58 +0200 Subject: [PATCH 10/41] Add to DAO --- .../prime/client/api/store/SubscriberDAO.kt | 3 +++ .../prime/client/api/store/SubscriberDAOImpl.kt | 15 +++++++++++++++ .../api/resources/ApplicationTokenResourceTest.kt | 11 ++++++----- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAO.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAO.kt index 21951565c..5cea776b8 100644 --- a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAO.kt +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAO.kt @@ -5,6 +5,7 @@ import io.vavr.control.Option import org.ostelco.prime.client.api.core.ApiError import org.ostelco.prime.client.api.model.Consent import org.ostelco.prime.client.api.model.SubscriptionStatus +import org.ostelco.prime.model.ApplicationToken import org.ostelco.prime.model.Product import org.ostelco.prime.model.Subscriber @@ -35,6 +36,8 @@ interface SubscriberDAO { fun reportAnalytics(subscriptionId: String, events: String): Option + fun storeApplicationToken(subscriptionId: String, token: ApplicationToken): Either + companion object { fun isValidProfile(profile: Subscriber?): Boolean { diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt index 6b093296f..a367ae371 100644 --- a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt @@ -6,6 +6,7 @@ import org.ostelco.prime.client.api.core.ApiError import org.ostelco.prime.client.api.model.Consent import org.ostelco.prime.client.api.model.SubscriptionStatus import org.ostelco.prime.logger +import org.ostelco.prime.model.ApplicationToken import org.ostelco.prime.model.Product import org.ostelco.prime.model.PurchaseRecord import org.ostelco.prime.model.Subscriber @@ -64,6 +65,20 @@ class SubscriberDAOImpl(private val storage: Storage, private val ocsSubscriberS return getProfile(subscriptionId) } + override fun storeApplicationToken(subscriptionId: String, token: ApplicationToken): Either { + + println("storeApplicationToken called") + val result = getMsisdn(subscriptionId) + + if (result.isRight) { + val msisdn = result.right().get() + storage.addNotificationToken(msisdn, token.token) + return Either.right(token) + } else { + return Either.left(ApiError("User not found")) + } + } + override fun updateProfile(subscriptionId: String, profile: Subscriber): Either { if (!SubscriberDAO.isValidProfile(profile)) { return Either.left(ApiError("Incomplete profile description")) diff --git a/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt b/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt index 5321ceb18..4e454254f 100644 --- a/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt +++ b/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt @@ -38,6 +38,8 @@ class ApplicationTokenResourceTest { private val applicationID = "myAppID:4378932" private val tokenType = "FCM" + private val applicationToken = ApplicationToken() + @Before @Throws(Exception::class) fun setUp() { @@ -51,10 +53,9 @@ class ApplicationTokenResourceTest { val arg1 = argumentCaptor() val arg2 = argumentCaptor() - /* - `when`(DAO.createProfile(arg1.capture(), arg2.capture())) - .thenReturn(Either.right(profile)) - */ + `when`(DAO.storeApplicationToken(arg1.capture(), arg2.capture())) + .thenReturn(Either.right(applicationToken)) + val resp = RULE.target("/applicationtoken") .request(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) @@ -84,7 +85,7 @@ class ApplicationTokenResourceTest { .setPrefix("Bearer") .buildAuthFilter())) .addResource(AuthValueFactoryProvider.Binder(AccessTokenPrincipal::class.java)) - .addResource(ProfileResource(DAO)) + .addResource(ApplicationTokenResource(DAO)) .setTestContainerFactory(GrizzlyWebTestContainerFactory()) .build() } From a28e087757e27cb714841412d285b3bd8f1015c9 Mon Sep 17 00:00:00 2001 From: Martin Cederlof Date: Fri, 8 Jun 2018 14:46:17 +0200 Subject: [PATCH 11/41] Updated ApplicationNotificationResourceTest --- .../client/api/resources/ApplicationTokenResource.kt | 7 ++++++- .../ostelco/prime/client/api/store/SubscriberDAO.kt | 2 +- .../prime/client/api/store/SubscriberDAOImpl.kt | 6 +++--- .../api/resources/ApplicationTokenResourceTest.kt | 11 +++++++++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt index 6d9315977..0a4a85e72 100644 --- a/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt @@ -27,7 +27,12 @@ class ApplicationTokenResource(private val dao: SubscriberDAO) : ResourceHelpers val msisdn = dao.getMsisdn(authToken.name) - println("ApplicationTokenResource called with msisdn : $msisdn and applicationToken : $applicationToken") + if (msisdn.isRight) { + val m = msisdn.right().get() + println("ApplicationTokenResource called with msisdn : $m") + } else { + println("ApplicationTokenResource could not find subscriper msisdn") + } val result = dao.getSubscriptionStatus(authToken.name) diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAO.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAO.kt index 5cea776b8..cc61f1fb1 100644 --- a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAO.kt +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAO.kt @@ -36,7 +36,7 @@ interface SubscriberDAO { fun reportAnalytics(subscriptionId: String, events: String): Option - fun storeApplicationToken(subscriptionId: String, token: ApplicationToken): Either + fun storeApplicationToken(subscriptionId: String, applicationToken: ApplicationToken): Either companion object { diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt index a367ae371..4b33ba661 100644 --- a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt @@ -65,15 +65,15 @@ class SubscriberDAOImpl(private val storage: Storage, private val ocsSubscriberS return getProfile(subscriptionId) } - override fun storeApplicationToken(subscriptionId: String, token: ApplicationToken): Either { + override fun storeApplicationToken(subscriptionId: String, applicationToken: ApplicationToken): Either { println("storeApplicationToken called") val result = getMsisdn(subscriptionId) if (result.isRight) { val msisdn = result.right().get() - storage.addNotificationToken(msisdn, token.token) - return Either.right(token) + storage.addNotificationToken(msisdn, applicationToken.token) + return Either.right(applicationToken) } else { return Either.left(ApiError("User not found")) } diff --git a/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt b/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt index 4e454254f..d5955056b 100644 --- a/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt +++ b/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt @@ -20,8 +20,8 @@ import org.ostelco.prime.client.api.core.ApiError import org.ostelco.prime.client.api.store.SubscriberDAO import org.ostelco.prime.client.api.util.AccessToken import org.ostelco.prime.model.ApplicationToken -import org.ostelco.prime.model.Subscriber import java.util.* +import javax.ws.rs.client.Client import javax.ws.rs.client.Entity import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response @@ -53,8 +53,12 @@ class ApplicationTokenResourceTest { val arg1 = argumentCaptor() val arg2 = argumentCaptor() + val argMsisdn = argumentCaptor() + val msisdn = "4790300001" + `when`(DAO.storeApplicationToken(arg1.capture(), arg2.capture())) .thenReturn(Either.right(applicationToken)) + `when`>(DAO.getMsisdn(argMsisdn.capture())).thenReturn(Either.right(msisdn)) val resp = RULE.target("/applicationtoken") .request(MediaType.APPLICATION_JSON) @@ -63,9 +67,10 @@ class ApplicationTokenResourceTest { .post(Entity.json("{\n" + " \"token\": \"" + token + "\",\n" + " \"applicationID\": \"" + applicationID + "\",\n" + - " \"tokenType\": \"" + tokenType + "\",\n" + + " \"tokenType\": \"" + tokenType + "\"\n" + "}\n")) + println("Response is $resp") assertThat(resp.status).isEqualTo(Response.Status.CREATED.statusCode) assertThat(resp.mediaType.toString()).isEqualTo(MediaType.APPLICATION_JSON) assertThat(arg1.firstValue).isEqualTo(email) @@ -75,6 +80,8 @@ class ApplicationTokenResourceTest { val DAO = mock(SubscriberDAO::class.java) val AUTHENTICATOR = mock(OAuthAuthenticator::class.java) + val PSEUDONYMENDPOINT = "http://localhost" + val client: Client = mock(Client::class.java) @JvmField @ClassRule From c14c383a8cbb996cd1120ec6db470be2a12fcf36 Mon Sep 17 00:00:00 2001 From: Martin Cederlof Date: Mon, 11 Jun 2018 15:54:38 +0200 Subject: [PATCH 12/41] Added integration test --- .../kotlin/org/ostelco/at/jersey/Tests.kt | 17 +++++++++++ .../prime/appnotifier/FirebaseAppNotifier.kt | 10 +++---- .../api/resources/ApplicationTokenResource.kt | 29 +++++++++---------- .../client/api/store/SubscriberDAOImpl.kt | 10 ++----- .../resources/ApplicationTokenResourceTest.kt | 7 +++-- .../prime/storage/firebase/FirebaseStorage.kt | 27 +++++++++++++---- .../org/ostelco/prime/model/Entities.kt | 15 +++++++++- .../ostelco/prime/storage/legacy/Storage.kt | 6 ++-- 8 files changed, 82 insertions(+), 39 deletions(-) diff --git a/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt b/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt index 470b8eb9f..3b4deabfd 100644 --- a/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt +++ b/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt @@ -8,7 +8,9 @@ import org.ostelco.prime.client.model.Product import org.ostelco.prime.client.model.Profile import org.ostelco.prime.client.model.SubscriptionStatus import org.ostelco.prime.logger +import org.ostelco.prime.model.ApplicationToken import java.time.Instant +import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull @@ -180,4 +182,19 @@ class ProfileTest { assertEquals("", clearedProfile.city, "Incorrect 'city' in response after clearing profile") assertEquals("", clearedProfile.country, "Incorrect 'country' in response after clearing profile") } + + @Test + fun testApplicationToken() { + + val token = UUID.randomUUID().toString() + val applicationId = "testApplicationId" + val tokenType = "FCM" + + val testToken = ApplicationToken(token, applicationId, tokenType) + + val reply: ApplicationToken = post { + path = "/applicationtoken" + body = testToken + } + } } \ No newline at end of file diff --git a/app-notifier/src/main/kotlin/org/ostelco/prime/appnotifier/FirebaseAppNotifier.kt b/app-notifier/src/main/kotlin/org/ostelco/prime/appnotifier/FirebaseAppNotifier.kt index 555d153ac..035e0f9c8 100644 --- a/app-notifier/src/main/kotlin/org/ostelco/prime/appnotifier/FirebaseAppNotifier.kt +++ b/app-notifier/src/main/kotlin/org/ostelco/prime/appnotifier/FirebaseAppNotifier.kt @@ -27,14 +27,16 @@ class FirebaseAppNotifier: AppNotifier { val store = getResource() // This registration token comes from the client FCM SDKs. - val applicationToken = store.getNotificationToken(msisdn) + val applicationTokens = store.getNotificationTokens(msisdn) - if (applicationToken != null) { + for (applicationToken in applicationTokens) { + + // ToDo : Currently we asume that all tokens are for FCM // See documentation on defining a message payload. val message = Message.builder() .setNotification(Notification(title, body)) - .setToken(applicationToken) + .setToken(applicationToken.token) .build() // Send a message to the device corresponding to the provided @@ -54,8 +56,6 @@ class FirebaseAppNotifier: AppNotifier { } addCallback(future, apiFutureCallback) - } else { - println("Not able to fetch notification token for msisdn : $msisdn") } } } \ No newline at end of file diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt index 0a4a85e72..72c92f913 100644 --- a/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt @@ -25,23 +25,22 @@ class ApplicationTokenResource(private val dao: SubscriberDAO) : ResourceHelpers .build() } - val msisdn = dao.getMsisdn(authToken.name) + val result = dao.getMsisdn(authToken.name) - if (msisdn.isRight) { - val m = msisdn.right().get() - println("ApplicationTokenResource called with msisdn : $m") + if (result.isRight) { + val msisdn = result.right().get() + val created = dao.storeApplicationToken(msisdn, applicationToken) + if (created.isRight) { + return Response.status(Response.Status.CREATED) + .entity(asJson(created.right().get())) + .build() + } else { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(asJson(created.left().get())) + .build() + } } else { - println("ApplicationTokenResource could not find subscriper msisdn") - } - - val result = dao.getSubscriptionStatus(authToken.name) - - return if (result.isRight) { - Response.status(Response.Status.CREATED) - .entity(asJson(result.right().get())) - .build() - } else { - Response.status(Response.Status.INTERNAL_SERVER_ERROR) + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(asJson(result.left().get())) .build() } diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt index 4b33ba661..682139fa6 100644 --- a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt @@ -66,16 +66,10 @@ class SubscriberDAOImpl(private val storage: Storage, private val ocsSubscriberS } override fun storeApplicationToken(subscriptionId: String, applicationToken: ApplicationToken): Either { - - println("storeApplicationToken called") - val result = getMsisdn(subscriptionId) - - if (result.isRight) { - val msisdn = result.right().get() - storage.addNotificationToken(msisdn, applicationToken.token) + if ( storage.addNotificationToken(subscriptionId, applicationToken) ) { return Either.right(applicationToken) } else { - return Either.left(ApiError("User not found")) + return Either.left(ApiError("Failed to store ApplicationToken")) } } diff --git a/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt b/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt index d5955056b..a0e83daed 100644 --- a/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt +++ b/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt @@ -70,17 +70,18 @@ class ApplicationTokenResourceTest { " \"tokenType\": \"" + tokenType + "\"\n" + "}\n")) - println("Response is $resp") assertThat(resp.status).isEqualTo(Response.Status.CREATED.statusCode) assertThat(resp.mediaType.toString()).isEqualTo(MediaType.APPLICATION_JSON) - assertThat(arg1.firstValue).isEqualTo(email) + assertThat(arg1.firstValue).isEqualTo(msisdn) + assertThat(arg2.firstValue.token).isEqualTo(token) + assertThat(arg2.firstValue.applicationID).isEqualTo(applicationID) + assertThat(arg2.firstValue.tokenType).isEqualTo(tokenType) } companion object { val DAO = mock(SubscriberDAO::class.java) val AUTHENTICATOR = mock(OAuthAuthenticator::class.java) - val PSEUDONYMENDPOINT = "http://localhost" val client: Client = mock(Client::class.java) @JvmField diff --git a/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt b/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt index b547ddef3..d52ba772a 100644 --- a/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt +++ b/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt @@ -9,6 +9,7 @@ import com.google.firebase.database.DatabaseReference import com.google.firebase.database.FirebaseDatabase import com.google.firebase.database.ValueEventListener import org.ostelco.prime.logger +import org.ostelco.prime.model.ApplicationToken import org.ostelco.prime.model.Product import org.ostelco.prime.model.PurchaseRecord import org.ostelco.prime.model.Subscriber @@ -89,12 +90,14 @@ object FirebaseStorageSingleton : Storage { } } - override fun addNotificationToken(msisdn: String, token: String) { - fcmTokenStore.create(msisdn, token) + override fun addNotificationToken(msisdn: String, token: ApplicationToken) : Boolean { + return fcmTokenStore.setInPath(token, msisdn, token.applicationID) } - override fun getNotificationToken(msisdn: String): String? { - return fcmTokenStore.get(msisdn) + override fun getNotificationTokens(msisdn: String): Collection { + return fcmTokenStore.getAll { + databaseReference.child(urlEncode(msisdn)) + }.values } } @@ -103,7 +106,7 @@ val productEntity = EntityType("products", Product::class.java) val subscriptionEntity = EntityType("subscriptions", String::class.java) val subscriberEntity = EntityType("subscribers", Subscriber::class.java) val paymentHistoryEntity = EntityType("paymentHistory", PurchaseRecord::class.java) -val fcmTokenEntity = EntityType("notificationTokens/FCM", String::class.java) +val fcmTokenEntity = EntityType("notificationTokens", ApplicationToken::class.java) val config = FirebaseConfigRegistry.firebaseConfig val firebaseDatabase = setupFirebaseInstance(config.databaseName, config.configFile) @@ -307,4 +310,18 @@ class EntityStore( future.get(TIMEOUT, SECONDS) ?: return false return true } + + fun setInPath(token: E, vararg childNodes: String): Boolean { + + var ref = databaseReference + + for (node in childNodes) { + ref = ref.child(urlEncode(node)) + } + + val future = ref.setValueAsync(token) + //future.get(TIMEOUT, SECONDS) ?: return false + future.get(TIMEOUT, SECONDS) + return true + } } \ No newline at end of file diff --git a/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt b/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt index 703adaa9d..39b932b82 100644 --- a/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt +++ b/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt @@ -41,7 +41,20 @@ data class Subscriber( data class ApplicationToken( var token: String = "", var applicationID: String = "", - var tokenType: String = "") + var tokenType: String = "") : Entity { + + constructor(applicationID: String) : this() { + this.applicationID = applicationID + } + + override var id: String + @JsonIgnore + get() = applicationID + @JsonIgnore + set(value) { + applicationID = value + } +} data class Price( var amount: Int = 0, diff --git a/prime-api/src/main/kotlin/org/ostelco/prime/storage/legacy/Storage.kt b/prime-api/src/main/kotlin/org/ostelco/prime/storage/legacy/Storage.kt index 73417cc98..fb53f3a4a 100644 --- a/prime-api/src/main/kotlin/org/ostelco/prime/storage/legacy/Storage.kt +++ b/prime-api/src/main/kotlin/org/ostelco/prime/storage/legacy/Storage.kt @@ -1,5 +1,6 @@ package org.ostelco.prime.storage.legacy +import org.ostelco.prime.model.ApplicationToken import org.ostelco.prime.model.Product import org.ostelco.prime.model.PurchaseRecord import org.ostelco.prime.model.Subscriber @@ -97,10 +98,11 @@ interface Storage { /** * Get token used for sending notification to user application */ - fun getNotificationToken(msisdn : String): String? + fun getNotificationTokens(msisdn : String): Collection /** * Add token used for sending notification to user application */ - fun addNotificationToken(msisdn: String, token: String) + fun addNotificationToken(msisdn: String, token: ApplicationToken) : Boolean + } From 726f1e7229cd13ae41d3e306e36722ad933e081e Mon Sep 17 00:00:00 2001 From: Martin Cederlof Date: Tue, 12 Jun 2018 10:53:16 +0200 Subject: [PATCH 13/41] Updated acceptance test for application token --- .../src/main/kotlin/org/ostelco/at/jersey/Tests.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt b/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt index 3b4deabfd..08f109d4c 100644 --- a/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt +++ b/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt @@ -196,5 +196,9 @@ class ProfileTest { path = "/applicationtoken" body = testToken } + + assertEquals(token, reply.token, "Incorrect token in reply after posting new token") + assertEquals(applicationId, reply.applicationID, "Incorrect applicationId in reply after posting new token") + assertEquals(tokenType, reply.tokenType, "Incorrect tokenType in reply after posting new token") } } \ No newline at end of file From 89b1829a32dfa149c7310e20db70c68b8d72f675 Mon Sep 17 00:00:00 2001 From: Martin Cederlof Date: Wed, 13 Jun 2018 13:25:02 +0200 Subject: [PATCH 14/41] Fixed return value for addin Token Fixed bug for set in EntityStore --- .../api/resources/ApplicationTokenResource.kt | 4 +-- .../client/api/store/SubscriberDAOImpl.kt | 22 ++++++++++--- .../prime/storage/firebase/FirebaseStorage.kt | 33 ++++++++----------- .../prime/disruptor/PrimeEventProducer.kt | 3 +- .../prime/disruptor/PrimeEventProducerImpl.kt | 2 +- .../ostelco/prime/storage/legacy/Storage.kt | 1 + prime/infra/prime-client-api.yaml | 6 +++- 7 files changed, 40 insertions(+), 31 deletions(-) diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt index 72c92f913..b9e902383 100644 --- a/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt @@ -35,12 +35,12 @@ class ApplicationTokenResource(private val dao: SubscriberDAO) : ResourceHelpers .entity(asJson(created.right().get())) .build() } else { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + return Response.status(507) // Insufficient Storage .entity(asJson(created.left().get())) .build() } } else { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + return Response.status(Response.Status.NOT_FOUND) .entity(asJson(result.left().get())) .build() } diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt index 682139fa6..b4b4643bb 100644 --- a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt @@ -65,12 +65,25 @@ class SubscriberDAOImpl(private val storage: Storage, private val ocsSubscriberS return getProfile(subscriptionId) } - override fun storeApplicationToken(subscriptionId: String, applicationToken: ApplicationToken): Either { - if ( storage.addNotificationToken(subscriptionId, applicationToken) ) { - return Either.right(applicationToken) - } else { + override fun storeApplicationToken(msisdn: String, applicationToken: ApplicationToken): Either { + try { + storage.addNotificationToken(msisdn, applicationToken) + } catch(e: Exception) { + LOG.error("Failed to store ApplicationToken", e) return Either.left(ApiError("Failed to store ApplicationToken")) } + return getNotificationToken(msisdn, applicationToken.applicationID) + } + + fun getNotificationToken(msisdn: String, applicationId: String): Either { + try { + return storage.getNotificationToken(msisdn, applicationId) + ?.let { Either.right(it) } + ?: return Either.left(ApiError("Failed to get ApplicationToken")) + } catch (e: StorageException) { + LOG.error("Failed to get ApplicationToken", e) + return Either.left(ApiError("Failed to get ApplicationToken")) + } } override fun updateProfile(subscriptionId: String, profile: Subscriber): Either { @@ -104,7 +117,6 @@ class SubscriberDAOImpl(private val storage: Storage, private val ocsSubscriberS LOG.error("Failed to get balance", e) return Either.left(ApiError("Failed to get balance")) } - } override fun getMsisdn(subscriptionId: String): Either { diff --git a/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt b/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt index d52ba772a..d518c8518 100644 --- a/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt +++ b/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt @@ -35,7 +35,7 @@ object FirebaseStorageSingleton : Storage { override val balances: Map get() = balanceStore.getAll() - override fun addSubscriber(id: String, subscriber: Subscriber) = subscriberStore.create(id, subscriber) + override fun addSubscriber(id: String, subscriber: Subscriber): Boolean = subscriberStore.create(id, subscriber) override fun getSubscriber(id: String): Subscriber? { val subscriber = subscriberStore.get(id) @@ -91,7 +91,11 @@ object FirebaseStorageSingleton : Storage { } override fun addNotificationToken(msisdn: String, token: ApplicationToken) : Boolean { - return fcmTokenStore.setInPath(token, msisdn, token.applicationID) + return fcmTokenStore.set(token.applicationID, token) { databaseReference.child(urlEncode(msisdn)) } + } + + override fun getNotificationToken(msisdn: String, applicationID: String): ApplicationToken? { + return fcmTokenStore.get(applicationID) { databaseReference.child(msisdn) } } override fun getNotificationTokens(msisdn: String): Collection { @@ -170,10 +174,10 @@ class EntityStore( * @param id * @return Entity */ - fun get(id: String): E? { + fun get(id: String, reference: EntityStore.() -> DatabaseReference = { databaseReference }): E? { var entity: E? = null val countDownLatch = CountDownLatch(1); - databaseReference.child(urlEncode(id)).addListenerForSingleValueEvent( + reference().child(urlEncode(id)).addListenerForSingleValueEvent( object : ValueEventListener { override fun onCancelled(error: DatabaseError?) { countDownLatch.countDown() @@ -267,6 +271,7 @@ class EntityStore( fun add(entity: E, reference: EntityStore.() -> DatabaseReference = { databaseReference }): String? { val newPushedEntry = reference().push() val future = newPushedEntry.setValueAsync(entity) + // FIXME this may always return null future.get(TIMEOUT, SECONDS) ?: return null return newPushedEntry.key } @@ -288,8 +293,9 @@ class EntityStore( * * @return success */ - fun set(id: String, entity: E): Boolean { - val future = databaseReference.child(urlEncode(id)).setValueAsync(entity) + fun set(id: String, entity: E, reference: EntityStore.() -> DatabaseReference = { databaseReference }): Boolean { + val future = reference().child(urlEncode(id)).setValueAsync(entity) + // FIXME this always return false future.get(TIMEOUT, SECONDS) ?: return false return true } @@ -307,21 +313,8 @@ class EntityStore( return false } val future = databaseReference.child(urlEncode(id)).removeValueAsync() + // FIXME this may always return false future.get(TIMEOUT, SECONDS) ?: return false return true } - - fun setInPath(token: E, vararg childNodes: String): Boolean { - - var ref = databaseReference - - for (node in childNodes) { - ref = ref.child(urlEncode(node)) - } - - val future = ref.setValueAsync(token) - //future.get(TIMEOUT, SECONDS) ?: return false - future.get(TIMEOUT, SECONDS) - return true - } } \ No newline at end of file diff --git a/ocs/src/main/kotlin/org/ostelco/prime/disruptor/PrimeEventProducer.kt b/ocs/src/main/kotlin/org/ostelco/prime/disruptor/PrimeEventProducer.kt index 12f8084cd..29cdfcdb3 100644 --- a/ocs/src/main/kotlin/org/ostelco/prime/disruptor/PrimeEventProducer.kt +++ b/ocs/src/main/kotlin/org/ostelco/prime/disruptor/PrimeEventProducer.kt @@ -10,8 +10,7 @@ interface PrimeEventProducer { fun releaseReservedDataBucketEvent( msisdn: String, bytes: Long) - - // FixMe : For now we assume that there is only 1 MSCC in the Request. + fun injectCreditControlRequestIntoRingbuffer( request: CreditControlRequestInfo, streamId: String) diff --git a/ocs/src/main/kotlin/org/ostelco/prime/disruptor/PrimeEventProducerImpl.kt b/ocs/src/main/kotlin/org/ostelco/prime/disruptor/PrimeEventProducerImpl.kt index adc2c7913..ca56af40e 100644 --- a/ocs/src/main/kotlin/org/ostelco/prime/disruptor/PrimeEventProducerImpl.kt +++ b/ocs/src/main/kotlin/org/ostelco/prime/disruptor/PrimeEventProducerImpl.kt @@ -87,7 +87,6 @@ class PrimeEventProducerImpl(private val ringBuffer: RingBuffer) : P requestedBytes = bytes) } - // FixMe : For now we assume that there is only 1 MSCC in the Request. override fun injectCreditControlRequestIntoRingbuffer( request: CreditControlRequestInfo, streamId: String) { @@ -98,6 +97,7 @@ class PrimeEventProducerImpl(private val ringBuffer: RingBuffer) : P streamId = streamId, requestId = request.requestId) } else { + // FixMe : For now we assume that there is only 1 MSCC in the Request. injectIntoRingbuffer(CREDIT_CONTROL_REQUEST, msisdn = request.msisdn, requestedBytes = request.getMscc(0).requested.totalOctets, diff --git a/prime-api/src/main/kotlin/org/ostelco/prime/storage/legacy/Storage.kt b/prime-api/src/main/kotlin/org/ostelco/prime/storage/legacy/Storage.kt index fb53f3a4a..de5effc9c 100644 --- a/prime-api/src/main/kotlin/org/ostelco/prime/storage/legacy/Storage.kt +++ b/prime-api/src/main/kotlin/org/ostelco/prime/storage/legacy/Storage.kt @@ -105,4 +105,5 @@ interface Storage { */ fun addNotificationToken(msisdn: String, token: ApplicationToken) : Boolean + fun getNotificationToken(msisdn: String, applicationID: String): ApplicationToken? } diff --git a/prime/infra/prime-client-api.yaml b/prime/infra/prime-client-api.yaml index 3c4f0bbc6..ea70cccd2 100644 --- a/prime/infra/prime-client-api.yaml +++ b/prime/infra/prime-client-api.yaml @@ -81,7 +81,11 @@ paths: responses: 201: description: "Successfully stored token." - 500: + schema: + $ref: '#/definitions/ApplicationToken' + 404: + description: "User not found." + 507: description: "Not able to store token." security: - auth0_jwt: [] From c23574cc671f308599f83447da11b4efc44edd8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Cederl=C3=B6f?= Date: Wed, 13 Jun 2018 13:37:38 +0200 Subject: [PATCH 15/41] Update README.md --- client-api/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/client-api/README.md b/client-api/README.md index 507ba158c..6ae3974bc 100644 --- a/client-api/README.md +++ b/client-api/README.md @@ -56,7 +56,6 @@ Furthermore: The API is developed partly through this document. Partly through the swagger specification of the prime/infra/prime-api.yaml file that is more or less reliably mirrored in the swagger-generated static website [swagger doc](https://ostelco.github.io/). -The specs are a bit in flux right now, but we expect it to settle down over the coming few weeks. ## Data model From d51779f965c769b1019c75971ed1ac9d7276cda3 Mon Sep 17 00:00:00 2001 From: Vihang Patil Date: Sat, 26 May 2018 10:32:43 +0200 Subject: [PATCH 16/41] Created embedded-graph-store & graph-store, and removed in-memory-store. --- .travis.yml | 4 +- .../org/ostelco/prime/admin/api/Model.kt | 14 - .../org/ostelco/prime/admin/api/Resources.kt | 108 ++++---- build.gradle | 1 - .../client/api/store/SubscriberDAOImpl.kt | 10 +- docker-compose.override.yaml | 8 + docs/README.md | 2 +- docs/TEST.md | 2 +- embedded-graph-store/build.gradle | 28 ++ .../storage/embeddedgraph/GraphModule.kt | 89 +++++++ .../prime/storage/embeddedgraph/GraphStore.kt | 166 ++++++++++++ .../prime/storage/embeddedgraph/Schema.kt | 239 ++++++++++++++++++ .../io.dropwizard.jackson.Discoverable | 1 + .../org.ostelco.prime.module.PrimeModule | 1 + .../org.ostelco.prime.storage.AdminDataStore | 1 + .../org.ostelco.prime.storage.legacy.Storage | 1 + .../storage/embeddedgraph/GraphStoreTest.kt | 100 ++++++++ .../embeddedgraph/ObjectHandlerTest.kt | 37 +++ .../prime/storage/embeddedgraph/SchemaTest.kt | 188 ++++++++++++++ firebase-store/scripts/create_balance.sh | 6 + .../scripts/create_subscriptions.sh | 7 + .../firebase/AbstractChildEventListener.kt | 33 --- .../firebase/AbstractValueEventListener.kt | 15 -- .../prime/storage/firebase/FirebaseStorage.kt | 124 ++++----- graph-store/README.md | 39 +++ graph-store/build.gradle | 30 +++ .../org/ostelco/prime/storage/graph/Graph.kt | 146 +++++++++++ .../prime/storage/graph/GraphModule.kt | 56 ++++ .../ostelco/prime/storage/graph/GraphStore.kt | 184 ++++++++++++++ .../org/ostelco/prime/storage/graph/Schema.kt | 239 ++++++++++++++++++ .../org.ostelco.prime.module.PrimeModule | 1 + .../org.ostelco.prime.storage.AdminDataStore | 1 + .../org.ostelco.prime.storage.legacy.Storage | 1 + in-memory-store/build.gradle | 12 - .../prime/storage/InMemoryDataStore.kt | 73 ------ .../org.ostelco.prime.storage.DataStore | 1 - .../org/ostelco/prime/model/Entities.kt | 51 +++- .../prime/handler/PurchaseRequestHandler.kt | 2 +- .../ostelco/prime/event/EventProcessorTest.kt | 2 +- .../handler/PurchaseRequestHandlerTest.kt | 2 +- .../ostelco/prime/storage/AdminDataStore.kt | 31 +++ .../org/ostelco/prime/storage/DataStore.kt | 33 --- .../ostelco/prime/storage/legacy/Storage.kt | 20 +- prime/build.gradle | 5 +- prime/config/config.yaml | 1 + prime/config/test.yaml | 1 + prime/infra/NEO4J.md | 27 ++ .../prime/storage/firebase/FbStorageTest.kt | 2 +- settings.gradle | 6 +- 49 files changed, 1814 insertions(+), 337 deletions(-) delete mode 100644 admin-api/src/main/kotlin/org/ostelco/prime/admin/api/Model.kt create mode 100644 embedded-graph-store/build.gradle create mode 100644 embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphModule.kt create mode 100644 embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphStore.kt create mode 100644 embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/Schema.kt create mode 100644 embedded-graph-store/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable create mode 100644 embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.module.PrimeModule create mode 100644 embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.storage.AdminDataStore create mode 100644 embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.storage.legacy.Storage create mode 100644 embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphStoreTest.kt create mode 100644 embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/ObjectHandlerTest.kt create mode 100644 embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/SchemaTest.kt create mode 100755 firebase-store/scripts/create_balance.sh create mode 100755 firebase-store/scripts/create_subscriptions.sh delete mode 100644 firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/AbstractChildEventListener.kt delete mode 100644 firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/AbstractValueEventListener.kt create mode 100644 graph-store/README.md create mode 100644 graph-store/build.gradle create mode 100644 graph-store/src/main/kotlin/org/ostelco/prime/storage/graph/Graph.kt create mode 100644 graph-store/src/main/kotlin/org/ostelco/prime/storage/graph/GraphModule.kt create mode 100644 graph-store/src/main/kotlin/org/ostelco/prime/storage/graph/GraphStore.kt create mode 100644 graph-store/src/main/kotlin/org/ostelco/prime/storage/graph/Schema.kt create mode 100644 graph-store/src/main/resources/META-INF/services/org.ostelco.prime.module.PrimeModule create mode 100644 graph-store/src/main/resources/META-INF/services/org.ostelco.prime.storage.AdminDataStore create mode 100644 graph-store/src/main/resources/META-INF/services/org.ostelco.prime.storage.legacy.Storage delete mode 100644 in-memory-store/build.gradle delete mode 100644 in-memory-store/src/main/kotlin/org/ostelco/prime/storage/InMemoryDataStore.kt delete mode 100644 in-memory-store/src/main/resources/META-INF/services/org.ostelco.prime.storage.DataStore create mode 100644 prime-api/src/main/kotlin/org/ostelco/prime/storage/AdminDataStore.kt delete mode 100644 prime-api/src/main/kotlin/org/ostelco/prime/storage/DataStore.kt create mode 100644 prime/infra/NEO4J.md diff --git a/.travis.yml b/.travis.yml index a5051417c..36e9626a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,11 +9,11 @@ before_install: # The curl command is not always working. Kept original command in comment incase codacy updates #- sudo apt-get install jq #- wget -O ~/codacy-coverage-reporter-assembly-latest.jar $(curl https://api.github.com/repos/codacy/codacy-coverage-reporter/releases/latest | jq -r .assets[0].browser_download_url) - - wget -O ~/codacy-coverage-reporter-assembly-latest.jar https://github.com/codacy/codacy-coverage-reporter/releases/download/4.0.0/codacy-coverage-reporter-4.0.0-assembly.jar + - wget -O ~/codacy-coverage-reporter-assembly-latest.jar https://github.com/codacy/codacy-coverage-reporter/releases/download/4.0.1/codacy-coverage-reporter-4.0.1-assembly.jar install: echo "skip 'gradle assemble' step" -script: ./gradlew build +script: ./gradlew build --parallel before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock diff --git a/admin-api/src/main/kotlin/org/ostelco/prime/admin/api/Model.kt b/admin-api/src/main/kotlin/org/ostelco/prime/admin/api/Model.kt deleted file mode 100644 index 4e470378f..000000000 --- a/admin-api/src/main/kotlin/org/ostelco/prime/admin/api/Model.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.ostelco.prime.admin.api - -import org.ostelco.prime.model.Entity - -class Offer : Entity { - override var id: String = "" - var segments: Array = emptyArray() - var products: Array = emptyArray() -} - -class Segment : Entity { - override var id: String = "" - var subscribers: Array = emptyArray() -} \ No newline at end of file diff --git a/admin-api/src/main/kotlin/org/ostelco/prime/admin/api/Resources.kt b/admin-api/src/main/kotlin/org/ostelco/prime/admin/api/Resources.kt index 3931384e2..0d2b6c02d 100644 --- a/admin-api/src/main/kotlin/org/ostelco/prime/admin/api/Resources.kt +++ b/admin-api/src/main/kotlin/org/ostelco/prime/admin/api/Resources.kt @@ -1,10 +1,13 @@ package org.ostelco.prime.admin.api +import org.ostelco.prime.model.Offer import org.ostelco.prime.model.Product import org.ostelco.prime.model.ProductClass +import org.ostelco.prime.model.Segment import org.ostelco.prime.module.getResource -import org.ostelco.prime.storage.DataStore +import org.ostelco.prime.storage.AdminDataStore +import org.ostelco.prime.storage.legacy.Storage import javax.ws.rs.GET import javax.ws.rs.POST import javax.ws.rs.PUT @@ -14,94 +17,97 @@ import javax.ws.rs.PathParam @Path("/offers") class OfferResource() { - private val dataStore by lazy { getResource() } + private val dataStore by lazy { getResource() } + private val adminDataStore by lazy { getResource() } - @GET - fun getOffers() = dataStore.getOffers().map { it.id } +// @GET +// fun getOffers() = adminDataStore.getOffers() - @GET - @Path("/{offer-id}") - fun getOffer(@PathParam("offer-id") offerId: String) = dataStore.getOffer(offerId) +// @GET +// @Path("/{offer-id}") +// fun getOffer(@PathParam("offer-id") offerId: String) = adminDataStore.getOffer(offerId) @POST - fun createOffer(offer: Offer) = dataStore.createOffer(toStoredOffer(offer)) - - private fun toStoredOffer(offer: Offer): org.ostelco.prime.model.Offer { - return org.ostelco.prime.model.Offer( - offer.id, - offer.segments.map { dataStore.getSegment(it) }.requireNoNulls(), - offer.products.map { dataStore.getProduct(it) }.requireNoNulls()) - } + fun createOffer(offer: Offer) = adminDataStore.createOffer(offer) + +// private fun toStoredOffer(offer: Offer): org.ostelco.prime.model.Offer { +// return org.ostelco.prime.model.Offer( +// offer.id, +// offer.segments.map { adminDataStore.getSegment(it) }.requireNoNulls(), +// offer.products.map { dataStore.getProduct(null, it) }.requireNoNulls()) +// } } @Path("/segments") class SegmentResource { - private val dataStore by lazy { getResource() } + private val dataStore by lazy { getResource() } + private val adminDataStore by lazy { getResource() } - @GET - fun getSegments() = dataStore.getSegments().map { it.id } +// @GET +// fun getSegments() = adminDataStore.getSegments().map { it.id } - @GET - @Path("/{segment-id}") - fun getSegment(@PathParam("segment-id") segmentId: String) = dataStore.getSegment(segmentId) +// @GET +// @Path("/{segment-id}") +// fun getSegment(@PathParam("segment-id") segmentId: String) = adminDataStore.getSegment(segmentId) @POST - fun createSegment(segment: Segment) = dataStore.createSegment(toStoredSegment(segment)) + fun createSegment(segment: Segment) = adminDataStore.createSegment(segment) @PUT @Path("/{segment-id}") fun updateSegment( @PathParam("segment-id") segmentId: String, - segment: Segment): Boolean { + segment: Segment) { segment.id = segmentId - return dataStore.updateSegment(toStoredSegment(segment)) + adminDataStore.updateSegment(segment) } - private fun toStoredSegment(segment: Segment): org.ostelco.prime.model.Segment { - return org.ostelco.prime.model.Segment( - segment.id, - segment.subscribers.map { dataStore.getSubscriber(it) }.requireNoNulls()) - } +// private fun toStoredSegment(segment: Segment): org.ostelco.prime.model.Segment { +// return org.ostelco.prime.model.Segment( +// segment.id, +// segment.subscribers.map { dataStore.getSubscriber(it) }.requireNoNulls()) +// } } @Path("/products") class ProductResource { - private val dataStore by lazy { getResource() } + private val dataStore by lazy { getResource() } + private val adminDataStore by lazy { getResource() } - @GET - fun getProducts() = dataStore.getProducts().map { it.id } +// @GET +// fun getProducts() = adminDataStore.getProducts().map { it.id } @GET @Path("/{product-sku}") - fun getProducts(@PathParam("product-sku") productSku: String) = dataStore.getProduct(productSku) + fun getProducts(@PathParam("product-sku") productSku: String) = dataStore.getProduct(null, productSku) @POST - fun createProduct(product: Product) = dataStore.createProduct(product) + fun createProduct(product: Product) = adminDataStore.createProduct(product) } @Path("/product_classes") class ProductClassResource { - private val dataStore by lazy { getResource() } - - @GET - fun getProductClasses() = dataStore.getProductClasses().map { it.id } + private val adminDataStore by lazy { getResource() } - @GET - @Path("/{product-class-id}") - fun getProductClass(@PathParam("product-class-id") productClassId: String) = dataStore.getProductClass(productClassId) +// @GET +// fun getProductClasses() = adminDataStore.getProductClasses().map { it.id } +// +// @GET +// @Path("/{product-class-id}") +// fun getProductClass(@PathParam("product-class-id") productClassId: String) = adminDataStore.getProductClass(productClassId) @POST - fun createProductClass(productClass: ProductClass) = dataStore.createProductClass(productClass) - - @PUT - @Path("/{product-class-id}") - fun updateProductClass( - @PathParam("product-class-id") productClassId: String, - productClass: ProductClass): Boolean { - return dataStore.updateProductClass( - productClass.copy(id = productClassId)) - } + fun createProductClass(productClass: ProductClass) = adminDataStore.createProductClass(productClass) + +// @PUT +// @Path("/{product-class-id}") +// fun updateProductClass( +// @PathParam("product-class-id") productClassId: String, +// productClass: ProductClass): Boolean { +// return adminDataStore.updateProductClass( +// productClass.copy(id = productClassId)) +// } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8ec28a86f..64b06af97 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,6 @@ allprojects { version = '1.0.0-SNAPSHOT' repositories { - mavenLocal() mavenCentral() jcenter() maven { url = "https://repository.jboss.org/nexus/content/repositories/releases/" } diff --git a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt index b4b4643bb..4cb3d71b8 100644 --- a/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAOImpl.kt @@ -50,7 +50,7 @@ class SubscriberDAOImpl(private val storage: Storage, private val ocsSubscriberS return Either.left(ApiError("Incomplete profile description")) } try { - storage.addSubscriber(subscriptionId, Subscriber( + storage.addSubscriber(Subscriber( profile.email, profile.name, profile.address, @@ -91,7 +91,7 @@ class SubscriberDAOImpl(private val storage: Storage, private val ocsSubscriberS return Either.left(ApiError("Incomplete profile description")) } try { - storage.updateSubscriber(subscriptionId, Subscriber( + storage.updateSubscriber(Subscriber( profile.email, profile.name, profile.address, @@ -135,7 +135,7 @@ class SubscriberDAOImpl(private val storage: Storage, private val ocsSubscriberS override fun getProducts(subscriptionId: String): Either> { try { - val products = storage.getProducts() + val products = storage.getProducts(subscriptionId) if (products.isEmpty()) { return Either.left(ApiError("No products found")) } @@ -152,7 +152,7 @@ class SubscriberDAOImpl(private val storage: Storage, private val ocsSubscriberS override fun purchaseProduct(subscriptionId: String, sku: String): Option { var msisdn: String? = null try { - msisdn = storage.getSubscription(subscriptionId) + msisdn = storage.getMsisdn(subscriptionId) } catch (e: StorageException) { LOG.error("Did not find subscription", e) } @@ -163,7 +163,7 @@ class SubscriberDAOImpl(private val storage: Storage, private val ocsSubscriberS val product: Product? try { - product = storage.getProduct(sku) + product = storage.getProduct(subscriptionId, sku) } catch (e: StorageException) { LOG.error("Did not find product: sku = $sku", e) return Option.of(ApiError("Product unavailable")) diff --git a/docker-compose.override.yaml b/docker-compose.override.yaml index fd6c88b4e..ca1071f7c 100644 --- a/docker-compose.override.yaml +++ b/docker-compose.override.yaml @@ -43,6 +43,14 @@ services: - "ext-pgw" ipv4_address: 172.16.238.2 + neo4j: + container_name: neo4j + image: neo4j + ports: + - "7474:7474" + - "7687:7687" + tmpfs: /data + pseudonym-server: container_name: pseudonym-server build: pseudonym-server diff --git a/docs/README.md b/docs/README.md index e85d7006f..6732bca77 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ [Deploying Project](./DEPLOY.md) -[Development](./DEPLOY.md) +[Development](./DEV.md) [Glossary](./GLOSSARY.md) diff --git a/docs/TEST.md b/docs/TEST.md index e18caf065..89e7eced1 100644 --- a/docs/TEST.md +++ b/docs/TEST.md @@ -2,7 +2,7 @@ ### Setup - * Configure firebase project - `pantel-tests` or `pantel-2decb` + * Configure firebase project - `pantel-2decb` * Save `pantel-prod.json` in all folders where this file is added in `.gitignore`. You can find these directories by executing the command: diff --git a/embedded-graph-store/build.gradle b/embedded-graph-store/build.gradle new file mode 100644 index 000000000..253607b05 --- /dev/null +++ b/embedded-graph-store/build.gradle @@ -0,0 +1,28 @@ +plugins { + id "java-library" + id "jacoco" + id "com.github.johnrengelman.shadow" version "2.0.4" + id "org.jetbrains.kotlin.jvm" version "1.2.41" + id "idea" + id "project-report" +} + +ext.neo4jVersion="3.4.0" + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation project(":prime-api") + implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.5" + implementation "com.fasterxml.jackson.core:jackson-databind:2.9.5" + + implementation "org.neo4j:neo4j-bolt:$neo4jVersion" + + testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlinVersion" + testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion" +} \ No newline at end of file diff --git a/embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphModule.kt b/embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphModule.kt new file mode 100644 index 000000000..95409858c --- /dev/null +++ b/embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphModule.kt @@ -0,0 +1,89 @@ +package org.ostelco.prime.storage.embeddedgraph + +import com.codahale.metrics.health.HealthCheck +import com.fasterxml.jackson.annotation.JsonTypeName +import io.dropwizard.lifecycle.Managed +import io.dropwizard.setup.Environment +import org.neo4j.graphdb.GraphDatabaseService +import org.neo4j.graphdb.factory.GraphDatabaseFactory +import org.neo4j.kernel.configuration.BoltConnector +import org.ostelco.prime.model.Price +import org.ostelco.prime.model.Product +import org.ostelco.prime.model.Subscriber +import org.ostelco.prime.module.PrimeModule +import java.io.File + +@JsonTypeName("embedded-graph") +class GraphModule : PrimeModule { + + override fun init(env: Environment) { + env.lifecycle().manage(GraphServer) + env.healthChecks().register("Embedded graph server", GraphServer) + + // starting explicitly since OCS needs it during its init() to load balance + GraphServer.start() + + // FIXME remove adding of dummy data in Graph DB + GraphStoreSingleton.createProduct(createProduct("1GB_249NOK", 24900)) + GraphStoreSingleton.createProduct(createProduct("2GB_299NOK", 29900)) + GraphStoreSingleton.createProduct(createProduct("3GB_349NOK", 34900)) + GraphStoreSingleton.createProduct(createProduct("5GB_399NOK", 39900)) + GraphStoreSingleton.addSubscription("foo@bar.com","4747900184") + GraphStoreSingleton.setBalance("4747900184",1_000_000_000L) + GraphStoreSingleton.addSubscriber(Subscriber(email = "foo@bar.com", name = "Test User")) + } +} + +object GraphServer : Managed, HealthCheck() { + + var bolt: BoltConnector = BoltConnector("0") + + lateinit var graphDb: GraphDatabaseService + private set + + private var isRunning: Boolean = false; + + override fun start() { + + if (isRunning) { + return + } + + graphDb = GraphDatabaseFactory() + .newEmbeddedDatabaseBuilder(File("build/neo4j")) + .setConfig(bolt.enabled, "true") + .setConfig(bolt.type, "BOLT") + .setConfig(bolt.listen_address, "localhost:7687") + .newGraphDatabase() + + isRunning = true + } + + override fun stop() { + graphDb.shutdown() + isRunning = false + } + + override fun check(): Result { + if (isRunning) { + return Result.healthy() + } else { + return Result.unhealthy("Embedded graph server not running") + } + } +} + +fun createProduct(sku: String, amount: Int): Product { + val product = Product() + product.sku = sku + product.price = Price() + product.price.amount = amount + product.price.currency = "NOK" + + // This is messy code + val gbs: Long = "${sku[0]}".toLong() + product.properties = mapOf("noOfBytes" to "${gbs*1024*1024*1024}") + product.presentation = mapOf("label" to "$gbs GB for ${amount/100}") + + return product +} \ No newline at end of file diff --git a/embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphStore.kt b/embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphStore.kt new file mode 100644 index 000000000..d3fe29f3a --- /dev/null +++ b/embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphStore.kt @@ -0,0 +1,166 @@ +package org.ostelco.prime.storage.embeddedgraph + +import org.ostelco.prime.model.ApplicationToken +import org.ostelco.prime.model.Entity +import org.ostelco.prime.model.Offer +import org.ostelco.prime.model.Product +import org.ostelco.prime.model.ProductClass +import org.ostelco.prime.model.PurchaseRecord +import org.ostelco.prime.model.Segment +import org.ostelco.prime.model.Subscriber +import org.ostelco.prime.model.Subscription +import org.ostelco.prime.storage.AdminDataStore +import org.ostelco.prime.storage.legacy.Storage +import org.ostelco.prime.storage.legacy.StorageException +import java.util.* +import java.util.stream.Collectors + +class GraphStore : Storage by GraphStoreSingleton, AdminDataStore by GraphStoreSingleton + +object GraphStoreSingleton : Storage, AdminDataStore { + + private val subscriberEntity = EntityType("Subscriber", Subscriber::class.java) + private val subscriberStore = EntityStore(subscriberEntity) + + private val productEntity = EntityType("Product", Product::class.java) + private val productStore = EntityStore(productEntity) + + private val subscriptionEntity = EntityType("Subscription", Subscription::class.java) + private val subscriptionStore = EntityStore(subscriptionEntity) + + private val notificationTokenEntity = EntityType("NotificationToken", ApplicationToken::class.java) + private val notificationTokenStore = EntityStore(notificationTokenEntity) + + private val subscriptionRelation = RelationType( + name = "HAS_SUBSCRIPTION", + from = subscriberEntity, + to = subscriptionEntity, + dataClass = Void::class.java) + private val subscriptionRelationStore = RelationStore(subscriptionRelation) + + private val purchaseRecordRelation = RelationType( + name = "PURCHASED", + from = subscriberEntity, + to = productEntity, + dataClass = PurchaseRecord::class.java) + private val purchaseRecordStore = RelationStore(purchaseRecordRelation) + + override val balances: Map + get() = subscriptionStore.getAll().mapValues { it.value.balance } + + override fun getSubscriber(id: String): Subscriber? = subscriberStore.get(id) + + override fun addSubscriber(subscriber: Subscriber): Boolean = subscriberStore.create(subscriber.id, subscriber) + + override fun updateSubscriber(subscriber: Subscriber): Boolean = subscriberStore.update(subscriber.id, subscriber) + + override fun removeSubscriber(id: String) = subscriberStore.delete(id) + + override fun addSubscription(id: String, msisdn: String): Boolean { + val from = subscriberStore.get(id) ?: return false + subscriptionStore.create(msisdn, Subscription(msisdn, 0L)) + val to = subscriptionStore.get(msisdn) ?: return false + return subscriptionRelationStore.create(from, null, to) + } + + override fun getProducts(subscriberId: String): Map { + val result = GraphServer.graphDb.execute( + """ + MATCH (:${subscriberEntity.name} {id: '$subscriberId'}) + <-[:${segmentToSubscriberRelation.name}]-(:${segmentEntity.name}) + <-[:${offerToSegmentRelation.name}]-(:${offerEntity.name}) + -[:${offerToProductRelation.name}]->(product:${productEntity.name}) + RETURN properties(product) AS product + """.trimIndent()) + + return result.stream() + .map { ObjectHandler.getObject(it["product"] as Map, Product::class.java) } + .collect(Collectors.toMap({ it?.sku }, { it })) + } + + override fun getProduct(subscriberId: String?, sku: String): Product? = productStore.get(sku) + + override fun getBalance(id: String): Long? { + return subscriberStore.getRelated(id, subscriptionRelation, subscriptionEntity) + .first() + .balance + } + + override fun setBalance(msisdn: String, noOfBytes: Long): Boolean = + subscriptionStore.update(msisdn, Subscription(msisdn, balance = noOfBytes)) + + override fun getMsisdn(subscriptionId: String): String? { + return subscriberStore.getRelated(subscriptionId, subscriptionRelation, subscriptionEntity) + .first() + .msisdn + } + + override fun getPurchaseRecords(id: String): Collection { + return subscriberStore.getRelations(id, purchaseRecordRelation) + } + + override fun addPurchaseRecord(id: String, purchase: PurchaseRecord): String? { + val subscriber = subscriberStore.get(id) ?: throw StorageException("Subscriber not found") + val product = productStore.get(purchase.product.sku) ?: throw StorageException("Product not found") + purchase.id = UUID.randomUUID().toString() + purchaseRecordStore.create(subscriber, purchase, product) + return purchase.id + } + + override fun getNotificationTokens(msisdn: String): Collection = notificationTokenStore.getAll().values + + override fun addNotificationToken(msisdn: String, token: ApplicationToken): Boolean = notificationTokenStore.create("$msisdn.${token.applicationID}", token) + + override fun getNotificationToken(msisdn: String, applicationID: String): ApplicationToken? = notificationTokenStore.get("$msisdn.$applicationID") + + // + // Admin Store + // + + private val offerEntity = EntityType("Offer", Entity::class.java) + private val offerStore = EntityStore(offerEntity) + + private val segmentEntity = EntityType("Segment", Entity::class.java) + private val segmentStore = EntityStore(segmentEntity) + + private val offerToSegmentRelation = RelationType("offerHasSegment", offerEntity, segmentEntity, Void::class.java) + private val offerToSegmentStore = RelationStore(offerToSegmentRelation) + + private val offerToProductRelation = RelationType("offerHasProduct", offerEntity, productEntity, Void::class.java) + private val offerToProductStore = RelationStore(offerToProductRelation) + + private val segmentToSubscriberRelation = RelationType("segmentToSubscriber", segmentEntity, subscriberEntity, Void::class.java) + private val segmentToSubscriberStore = RelationStore(segmentToSubscriberRelation) + + private val productClassEntity = EntityType("ProductClass", ProductClass::class.java) + private val productClassStore = EntityStore(productClassEntity) + + override fun createProductClass(productClass: ProductClass): Boolean = productClassStore.create(productClass.id, productClass) + + override fun createProduct(product: Product): Boolean = productStore.create(product.sku, product) + + override fun createSegment(segment: Segment) { + segmentStore.create(segment.id, segment) + updateSegment(segment) + } + + override fun createOffer(offer: Offer) { + offerStore.create(offer.id, offer) + offerToSegmentStore.create(offer.id, offer.segments) + offerToProductStore.create(offer.id, offer.products) + } + + override fun updateSegment(segment: Segment) { + segmentToSubscriberStore.create(segment.id, segment.subscribers) + } + + // override fun getOffers(): Collection = offerStore.getAll().values.map { Offer().apply { id = it.id } } + + // override fun getSegments(): Collection = segmentStore.getAll().values.map { Segment().apply { id = it.id } } + + // override fun getOffer(id: String): Offer? = offerStore.get(id)?.let { Offer().apply { this.id = it.id } } + + // override fun getSegment(id: String): Segment? = segmentStore.get(id)?.let { Segment().apply { this.id = it.id } } + + // override fun getProductClass(id: String): ProductClass? = productClassStore.get(id) +} \ No newline at end of file diff --git a/embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/Schema.kt b/embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/Schema.kt new file mode 100644 index 000000000..8f885a57e --- /dev/null +++ b/embedded-graph-store/src/main/kotlin/org/ostelco/prime/storage/embeddedgraph/Schema.kt @@ -0,0 +1,239 @@ +package org.ostelco.prime.storage.embeddedgraph + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import org.neo4j.graphdb.Label +import org.neo4j.graphdb.Node +import org.neo4j.graphdb.Relationship +import org.neo4j.graphdb.RelationshipType +import org.neo4j.graphdb.Transaction +import org.ostelco.prime.logger +import org.ostelco.prime.model.HasId +import org.ostelco.prime.storage.embeddedgraph.ObjectHandler.getProperties +import java.util.stream.Collectors + +// +// Schema classes +// + +data class EntityType( + val name: String, + private val dataClass: Class) { + + fun createEntity(map: Map): ENTITY = ObjectHandler.getObject(map, dataClass) +} + +data class RelationType( + val name: String, + val from: EntityType, + val to: EntityType, + private val dataClass: Class?) { + + fun createRelation(map: Map): RELATION? { + return ObjectHandler.getObject(map, dataClass ?: return null) + } +} + +class EntityStore(private val entityType: EntityType) { + + private val LOG by logger() + + fun getAll(): Map { + return GraphServer.graphDb + .findNodes(Label.label(entityType.name)) + .stream() + .collect(Collectors.toMap( + { it.getProperty("id") as String }, + { entityType.createEntity(it.allProperties) })) + + } + + fun get(id: String): E? = getNode(id)?.let { entityType.createEntity(it.allProperties) } + + fun getNode(id: String): Node? = entityType.name.getGraphNode(id) + + fun create(id: String, entity: E): Boolean { + var transaction: Transaction? = null + try { + transaction = GraphServer.graphDb.beginTx() + val node = GraphServer.graphDb.createNode(Label.label(entityType.name)) + getProperties(entity).forEach { + node.setProperty(it.key, it.value) + } + node.setProperty("id", id) + transaction.success() + return true + } catch (e: Exception) { + LOG.error("Failed to create ${entityType.name}", e) + transaction?.failure() + return false + } + } + + fun getRelated(id: String, relationType: RelationType, toEntityType: EntityType): List { + var transaction: Transaction? = null + try { + transaction = GraphServer.graphDb.beginTx() + return entityType.name.getGraphRelatedNodes(id, relationType.name) + .map { it.allProperties } + .map { toEntityType.createEntity(it) } + } finally { + transaction?.close() + } + } + + fun getRelations(id: String, relationType: RelationType): List { + return entityType.name.getGraphRelations(id, relationType.name) + .map { relationType.createRelation(it.allProperties) } + .filterNotNull() + } + + fun update(id: String, entity: E): Boolean { + var transaction: Transaction? = null + try { + transaction = GraphServer.graphDb.beginTx() + val node = getNode(id) ?: return false + getProperties(entity).forEach { + node.setProperty(it.key, it.value) + } + transaction.success() + return true + } catch (e: Exception) { + LOG.error("Failed to create ${entityType.name}", e) + transaction?.failure() + return false + } finally { + transaction?.close() + } + } + + fun delete(id: String): Boolean { + val node = getNode(id) ?: return false + node.delete() + return true + } +} + +class RelationStore(private val relationType: RelationType) { + + private val LOG by logger() + + fun create(from: FROM, relation: Any?, to: TO): Boolean { + var transaction: Transaction? = null + try { + transaction = GraphServer.graphDb.beginTx() + val fromNode = relationType.from.name.getGraphNode(from.id) ?: return false + val toNode = relationType.to.name.getGraphNode(to.id) ?: return false + + val relationship = fromNode.createRelationshipTo(toNode, RelationshipType.withName(relationType.name)) + if (relation != null) { + getProperties(relation).forEach { + relationship.setProperty(it.key, it.value) + } + } + transaction.success() + return true + } catch (e: Exception) { + LOG.error("Failed to create ${relationType.name}", e) + transaction?.failure() + return false + } finally { + transaction?.close() + } + } + + fun create(fromId: String, toIds: Collection) { + relationType.from.name.createRelationsTo( + fromId = fromId, + relation = relationType.name, + toLabel = relationType.to.name, + toIds = toIds) + } +} + +// +// String extension functions +// + +fun String.getGraphNode(id: String): Node? = GraphServer.graphDb.findNode(Label.label(this), "id", id) + +fun String.getGraphRelations(id: String, relation: String): Collection { + return this.getGraphNode(id) + ?.getRelationships(RelationshipType.withName(relation)) + ?.toList() ?: emptyList() +} + +fun String.getGraphRelatedNodes(id: String, relation: String): Collection { + return this.getGraphRelations(id, relation) + .map { it.endNode } +} + +fun String.createRelationsTo(fromId: String, relation: String, toLabel: String, toIds: Collection): Boolean { + + var transaction: Transaction? = null + try { + transaction = GraphServer.graphDb.beginTx() + val fromNode = getGraphNode(fromId) ?: return false + val relationType = RelationshipType.withName(relation) + // delete existing relations. So, this function can be used for update too. + fromNode.getRelationships(relationType).forEach { it.delete() } + toIds.map { toLabel.getGraphNode(it) } + .map { fromNode.createRelationshipTo(it, relationType) } + transaction.success() + return true + } catch (e: Exception) { + val logger by logger() + logger.error("Failed to create $relation", e) + transaction?.failure() + return false + } +} + +// +// Object mapping functions +// +object ObjectHandler { + + private const val SEPARATOR = '/' + + private val objectMapper = ObjectMapper().registerKotlinModule() + + fun getProperties(any: Any): Map = toSimpleMap( + objectMapper.convertValue(any, object : TypeReference>() {})) + + private fun toSimpleMap(map: Map, prefix: String = ""): Map { + val outputMap: MutableMap = LinkedHashMap() + map.forEach { key, value -> + when (value) { + is Map<*, *> -> outputMap.putAll(toSimpleMap(value as Map, "$prefix$key$SEPARATOR")) + is List<*> -> println("Skipping list value: $value") + else -> outputMap["$prefix$key"] = value + } + } + return outputMap + } + + fun getObject(map: Map, dataClass: Class): D { + return objectMapper.convertValue(toNestedMap(map), dataClass) + } + + internal fun toNestedMap(map: Map): Map { + val outputMap: MutableMap = LinkedHashMap() + map.forEach { key, value -> + if (key.contains(SEPARATOR)) { + val keys = key.split(SEPARATOR) + var loopMap = outputMap + for (i in 0..(keys.size - 2)) { + loopMap.putIfAbsent(keys[i], LinkedHashMap()) + loopMap = loopMap[keys[i]] as MutableMap + } + loopMap[keys.last()] = value + + } else { + outputMap[key] = value + } + } + return outputMap + } +} \ No newline at end of file diff --git a/embedded-graph-store/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable b/embedded-graph-store/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable new file mode 100644 index 000000000..8056fe23b --- /dev/null +++ b/embedded-graph-store/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable @@ -0,0 +1 @@ +org.ostelco.prime.module.PrimeModule \ No newline at end of file diff --git a/embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.module.PrimeModule b/embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.module.PrimeModule new file mode 100644 index 000000000..ed8eccde6 --- /dev/null +++ b/embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.module.PrimeModule @@ -0,0 +1 @@ +org.ostelco.prime.storage.embeddedgraph.GraphModule \ No newline at end of file diff --git a/embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.storage.AdminDataStore b/embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.storage.AdminDataStore new file mode 100644 index 000000000..ec79565a3 --- /dev/null +++ b/embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.storage.AdminDataStore @@ -0,0 +1 @@ +org.ostelco.prime.storage.embeddedgraph.GraphStore \ No newline at end of file diff --git a/embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.storage.legacy.Storage b/embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.storage.legacy.Storage new file mode 100644 index 000000000..ec79565a3 --- /dev/null +++ b/embedded-graph-store/src/main/resources/META-INF/services/org.ostelco.prime.storage.legacy.Storage @@ -0,0 +1 @@ +org.ostelco.prime.storage.embeddedgraph.GraphStore \ No newline at end of file diff --git a/embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphStoreTest.kt b/embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphStoreTest.kt new file mode 100644 index 000000000..2b854043a --- /dev/null +++ b/embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/GraphStoreTest.kt @@ -0,0 +1,100 @@ +package org.ostelco.prime.storage.embeddedgraph + +import org.junit.AfterClass +import org.junit.BeforeClass +import org.ostelco.prime.model.Offer +import org.ostelco.prime.model.PurchaseRecord +import org.ostelco.prime.model.Segment +import org.ostelco.prime.model.Subscriber +import java.time.Instant +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class GraphStoreTest { + + @BeforeTest + fun clear() { + GraphServer.graphDb.execute("MATCH (n) DETACH DELETE n") + } + + @Test + fun `test add subscriber`() { + assertTrue(GraphStoreSingleton.addSubscriber(Subscriber(email = EMAIL, name = NAME))) + assertEquals( + Subscriber(email = EMAIL, name = NAME), + GraphStoreSingleton.getSubscriber(EMAIL)) + } + + @Test + fun `test add subscription, set and get balance`() { + assert(GraphStoreSingleton.addSubscriber(Subscriber(email = EMAIL, name = NAME))) + + assertTrue(GraphStoreSingleton.addSubscription(EMAIL, MSISDN)) + assertEquals(MSISDN, GraphStoreSingleton.getMsisdn(EMAIL)) + + GraphStoreSingleton.setBalance(MSISDN, BALANCE) + assertEquals(BALANCE, GraphStoreSingleton.getBalance(EMAIL)) + } + + @Test + fun `test set and get Purchase record`() { + assert(GraphStoreSingleton.addSubscriber(Subscriber(email = EMAIL, name = NAME))) + + val product = createProduct("1GB_249NOK", 24900) + val now = Instant.now().toEpochMilli() + + assertTrue(GraphStoreSingleton.createProduct(product), "Failed to create product") + + val purchaseRecord = PurchaseRecord(MSISDN, product, now) + assertNotNull(GraphStoreSingleton.addPurchaseRecord(EMAIL, purchaseRecord), "Failed to add purchase record") + + assertEquals(listOf(purchaseRecord), GraphStoreSingleton.getPurchaseRecords(EMAIL)) + } + + @Test + fun `test offer, segment and get products`() { + assert(GraphStoreSingleton.addSubscriber(Subscriber(email = EMAIL, name = NAME))) + + GraphStoreSingleton.createProduct(createProduct("1GB_249NOK", 24900)) + GraphStoreSingleton.createProduct(createProduct("2GB_299NOK", 29900)) + GraphStoreSingleton.createProduct(createProduct("3GB_349NOK", 34900)) + GraphStoreSingleton.createProduct(createProduct("5GB_399NOK", 39900)) + + val segment = Segment() + segment.id = "NEW_SEGMENT" + segment.subscribers = listOf(EMAIL) + GraphStoreSingleton.createSegment(segment) + + val offer = Offer() + offer.id = "NEW_OFFER" + offer.segments = listOf("NEW_SEGMENT") + offer.products = listOf("3GB_349NOK") + GraphStoreSingleton.createOffer(offer) + + val products = GraphStoreSingleton.getProducts(EMAIL) + assertEquals(1, products.size) + assertEquals(createProduct("3GB_349NOK", 34900), products.values.first()) + } + + companion object { + const val EMAIL = "foo@bar.com" + const val NAME = "Test User" + const val MSISDN = "4712345678" + const val BALANCE = 12345L + + @BeforeClass + @JvmStatic + fun start() { + GraphServer.start() + } + + @AfterClass + @JvmStatic + fun stop() { + GraphServer.stop() + } + } +} \ No newline at end of file diff --git a/embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/ObjectHandlerTest.kt b/embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/ObjectHandlerTest.kt new file mode 100644 index 000000000..4ff077643 --- /dev/null +++ b/embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/ObjectHandlerTest.kt @@ -0,0 +1,37 @@ +package org.ostelco.prime.storage.embeddedgraph + +import kotlin.test.Test +import kotlin.test.assertEquals + +class ObjectHandlerTest { + + @Test + fun `test object to map and back`() { + val map = ObjectHandler.getProperties(createProduct("1GB_249NOK", 24900)) + + val expectedMap = LinkedHashMap() + expectedMap["sku"] = "1GB_249NOK" + expectedMap["price/amount"] = 24900 + expectedMap["price/currency"] = "NOK" + expectedMap["properties/noOfBytes"] = "1073741824" + expectedMap["presentation/label"] = "1 GB for 249" + + assertEquals(expectedMap, map) + + val expectedNestedMap = LinkedHashMap() + expectedNestedMap["sku"] = "1GB_249NOK" + val priceMap = LinkedHashMap() + expectedNestedMap["price"] = priceMap + priceMap["amount"] = 24900 + priceMap["currency"] = "NOK" + val propertiesMap = LinkedHashMap() + expectedNestedMap["properties"] = propertiesMap + propertiesMap["noOfBytes"] = "1073741824" + val presentationMap = LinkedHashMap() + expectedNestedMap["presentation"] = presentationMap + presentationMap["label"] = "1 GB for 249" + + val nestedMap = ObjectHandler.toNestedMap(map) + assertEquals(expectedNestedMap, nestedMap) + } +} \ No newline at end of file diff --git a/embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/SchemaTest.kt b/embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/SchemaTest.kt new file mode 100644 index 000000000..d011f57b4 --- /dev/null +++ b/embedded-graph-store/src/test/kotlin/org/ostelco/prime/storage/embeddedgraph/SchemaTest.kt @@ -0,0 +1,188 @@ +package org.ostelco.prime.storage.embeddedgraph + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.AfterClass +import org.junit.BeforeClass +import org.junit.Test +import org.ostelco.prime.model.HasId +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class SchemaTest { + + @Test + fun `test node`() { + + val aId = "a_id" + val aEntity = EntityType("A", A::class.java) + val aEntityStore = EntityStore(aEntity) + + // create node + val a = A() + a.id = aId + a.field1 = "value1" + a.field2 = "value2" + + aEntityStore.create(aId, a) + + // get node + assertEquals(a, aEntityStore.get("a_id")) + + // update node + val ua = A() + ua.id = aId + ua.field1 = "value1_u" + ua.field2 = "value2_u" + + aEntityStore.update(aId, ua) + + // get updated node + assertEquals(ua, aEntityStore.get(aId)) + + // delete node + aEntityStore.delete(aId) + + // get deleted node + assertNull(aEntityStore.get(aId)) + } + + @Test + fun `test related node`() { + + val aId = "a_id" + val bId = "b_id" + + val fromEntity = EntityType("From", A::class.java) + val fromEntityStore = EntityStore(fromEntity) + + val toEntity = EntityType("To", B::class.java) + val toEntityStore = EntityStore(toEntity) + + val relation = RelationType("relatedTo", fromEntity, toEntity, null) + val relationStore = RelationStore(relation) + + // create nodes + val a = A() + a.id = aId + a.field1 = "a's value1" + a.field2 = "a's value2" + + val b = B() + b.id = bId + b.field1 = "b's value1" + b.field2 = "b's value2" + + fromEntityStore.create(aId, a) + toEntityStore.create(bId, b) + + // create relation + relationStore.create(a, null, b) + + // get 'b' from 'a' + assertEquals(listOf(b), fromEntityStore.getRelated(aId, relation, toEntity)) + } + + @Test + fun `test relation with properties`() { + + val aId = "a_id" + val bId = "b_id" + + val fromEntity = EntityType("From2", A::class.java) + val fromEntityStore = EntityStore(fromEntity) + + val toEntity = EntityType("To2", B::class.java) + val toEntityStore = EntityStore(toEntity) + + val relation = RelationType("relatedTo", fromEntity, toEntity, R::class.java) + val relationStore = RelationStore(relation) + + // create nodes + val a = A() + a.id = aId + a.field1 = "a's value1" + a.field2 = "a's value2" + + val b = B() + b.id = bId + b.field1 = "b's value1" + b.field2 = "b's value2" + + fromEntityStore.create(aId, a) + toEntityStore.create(bId, b) + + // create relation + val r = R() + r.field1 = "r's value1" + r.field2 = "r's value2" + relationStore.create(a, r, b) + + // get 'b' from 'a' + assertEquals(listOf(b), fromEntityStore.getRelated(aId, relation, toEntity)) + + // get 'r' from 'a' + assertEquals(listOf(r), fromEntityStore.getRelations(aId, relation)) + } + + @Test + fun `json to map`() { + val objectMapper = ObjectMapper() + val map = objectMapper.readValue>("""{"label":"3GB for 300 NOK"}""", object : TypeReference>() {}) + assertEquals("3GB for 300 NOK", map["label"]) + } + + companion object { + + @BeforeClass + @JvmStatic + fun start() { + GraphServer.start() + } + + @AfterClass + @JvmStatic + fun stop() { + GraphServer.stop() + } + } +} + +data class A( + var field1: String? = null, + var field2: String? = null) : HasId { + + private var _id: String = "" + + override var id: String + get() = _id + set(value) { + _id = value + } +} + +data class B( + var field1: String? = null, + var field2: String? = null) : HasId { + + private var _id: String = "" + + override var id: String + get() = _id + set(value) { + _id = value + } +} + +data class R( + var field1: String? = null, + var field2: String? = null) : HasId { + + private var _id: String = "" + + override var id: String + get() = _id + set(value) { + _id = value + } +} \ No newline at end of file diff --git a/firebase-store/scripts/create_balance.sh b/firebase-store/scripts/create_balance.sh new file mode 100755 index 000000000..5a2160d71 --- /dev/null +++ b/firebase-store/scripts/create_balance.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +for MSISDN in {178..190} +do + echo firebase --project pantel-2decb --data '0' database:set /v2/balance/4790300${MSISDN} +done diff --git a/firebase-store/scripts/create_subscriptions.sh b/firebase-store/scripts/create_subscriptions.sh new file mode 100755 index 000000000..decf92cf1 --- /dev/null +++ b/firebase-store/scripts/create_subscriptions.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +export MSISDN= +export EMAIL= + +export URL_ENCODED_EMAIL=$(echo "$EMAIL" | sed 's/\./%2E/g' | sed 's/@/%40/g') +echo firebase --project pantel-2decb --data "\"$MSISDN\"" database:set /v2/subscriptions/"$URL_ENCODED_EMAIL" diff --git a/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/AbstractChildEventListener.kt b/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/AbstractChildEventListener.kt deleted file mode 100644 index 86d808cc2..000000000 --- a/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/AbstractChildEventListener.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.ostelco.prime.storage.firebase - -import com.google.firebase.database.ChildEventListener -import com.google.firebase.database.DataSnapshot -import com.google.firebase.database.DatabaseError - - -/** - * Convenience class, so that in classes that actually do anything, it's only necessary - * to implement those methods that actually do anything. - */ -abstract class AbstractChildEventListener : ChildEventListener { - - override fun onChildAdded(dataSnapshot: DataSnapshot, prevChildKey: String?) { - // Intended to be overridden in by subclass. Default is to do nothing. - } - - override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) { - // Intended to be overridden in by subclass. Default is to do nothing. - } - - override fun onChildRemoved(snapshot: DataSnapshot) { - // Intended to be overridden in by subclass. Default is to do nothing. - } - - override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) { - // Intended to be overridden in by subclass. Default is to do nothing. - } - - override fun onCancelled(error: DatabaseError) { - // Intended to be overridden in by subclass. Default is to do nothing. - } -} diff --git a/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/AbstractValueEventListener.kt b/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/AbstractValueEventListener.kt deleted file mode 100644 index 0e7bad1eb..000000000 --- a/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/AbstractValueEventListener.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.ostelco.prime.storage.firebase - -import com.google.firebase.database.DataSnapshot -import com.google.firebase.database.DatabaseError -import com.google.firebase.database.ValueEventListener - -abstract class AbstractValueEventListener : ValueEventListener { - override fun onDataChange(snapshot: DataSnapshot) { - // Intentionally left blank. - } - - override fun onCancelled(error: DatabaseError) { - // Intentionally left blank. - } -} diff --git a/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt b/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt index d518c8518..b5ebbed07 100644 --- a/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt +++ b/firebase-store/src/main/kotlin/org/ostelco/prime/storage/firebase/FirebaseStorage.kt @@ -32,10 +32,58 @@ class FirebaseStorage : Storage by FirebaseStorageSingleton object FirebaseStorageSingleton : Storage { + private val balanceEntity = EntityType("balance", Long::class.java) + private val productEntity = EntityType("products", Product::class.java) + private val subscriptionEntity = EntityType("subscriptions", String::class.java) + private val subscriberEntity = EntityType("subscribers", Subscriber::class.java) + private val paymentHistoryEntity = EntityType("paymentHistory", PurchaseRecord::class.java) + private val fcmTokenEntity = EntityType("notificationTokens", ApplicationToken::class.java) + + private val firebaseDatabase = setupFirebaseInstance(config.databaseName, config.configFile) + + private val balanceStore = EntityStore(firebaseDatabase, balanceEntity) + private val productStore = EntityStore(firebaseDatabase, productEntity) + private val subscriptionStore = EntityStore(firebaseDatabase, subscriptionEntity) + private val subscriberStore = EntityStore(firebaseDatabase, subscriberEntity) + private val paymentHistoryStore = EntityStore(firebaseDatabase, paymentHistoryEntity) + private val fcmTokenStore = EntityStore(firebaseDatabase, fcmTokenEntity) + + private fun setupFirebaseInstance( + databaseName: String, + configFile: String): FirebaseDatabase { + + try { + + val credentials: GoogleCredentials = if (Files.exists(Paths.get(configFile))) { + FileInputStream(configFile).use { serviceAccount -> GoogleCredentials.fromStream(serviceAccount) } + } else { + GoogleCredentials.getApplicationDefault() + } + + val options = FirebaseOptions.Builder() + .setCredentials(credentials) + .setDatabaseUrl("https://$databaseName.firebaseio.com/") + .build() + try { + FirebaseApp.getInstance() + } catch (e: Exception) { + FirebaseApp.initializeApp(options) + } + + return FirebaseDatabase.getInstance() + + // (un)comment next line to turn on/of extended debugging + // from firebase. + // this.firebaseDatabase.setLogLevel(com.google.firebase.database.Logger.Level.DEBUG); + } catch (ex: IOException) { + throw StorageException(ex) + } + } + override val balances: Map get() = balanceStore.getAll() - override fun addSubscriber(id: String, subscriber: Subscriber): Boolean = subscriberStore.create(id, subscriber) + override fun addSubscriber(subscriber: Subscriber) = subscriberStore.create(subscriber.id, subscriber) override fun getSubscriber(id: String): Subscriber? { val subscriber = subscriberStore.get(id) @@ -43,20 +91,21 @@ object FirebaseStorageSingleton : Storage { return subscriber } - override fun updateSubscriber(id: String, subscriber: Subscriber): Boolean = subscriberStore.update(id, subscriber) - - override fun getSubscription(id: String) = subscriptionStore.get(id) + override fun updateSubscriber(subscriber: Subscriber): Boolean = subscriberStore.update(subscriber.id, subscriber) override fun getMsisdn(subscriptionId: String) = subscriptionStore.get(subscriptionId) - override fun addSubscription(id: String, msisdn: String) { - subscriptionStore.create(id, msisdn) - balanceStore.create(msisdn, 0) + override fun addSubscription(id: String, msisdn: String): Boolean { + if (subscriptionStore.create(id, msisdn)) { + // should we set non-zero default balance? + return balanceStore.create(msisdn, 0) + } + return false } - override fun getProduct(sku: String) = productStore.get(sku) + override fun getProduct(subscriberId: String?, sku: String) = productStore.get(sku) - override fun getProducts() = productStore.getAll() + override fun getProducts(subscriberId: String): Map = productStore.getAll() override fun getBalance(id: String): Long? { val msisdn = subscriptionStore.get(id) ?: return null @@ -79,15 +128,17 @@ object FirebaseStorageSingleton : Storage { } } - override fun removeSubscriber(id: String) { + override fun removeSubscriber(id: String): Boolean { subscriberStore.delete(id) // for payment history, skip checking if it exists. paymentHistoryStore.delete(id, dontExists = false) val msisdn = subscriptionStore.get(id) if (msisdn != null) { - subscriptionStore.delete(id) - balanceStore.delete(msisdn) + if (subscriptionStore.delete(id)) { + return balanceStore.delete(msisdn) + } } + return false } override fun addNotificationToken(msisdn: String, token: ApplicationToken) : Boolean { @@ -105,54 +156,7 @@ object FirebaseStorageSingleton : Storage { } } -val balanceEntity = EntityType("balance", Long::class.java) -val productEntity = EntityType("products", Product::class.java) -val subscriptionEntity = EntityType("subscriptions", String::class.java) -val subscriberEntity = EntityType("subscribers", Subscriber::class.java) -val paymentHistoryEntity = EntityType("paymentHistory", PurchaseRecord::class.java) -val fcmTokenEntity = EntityType("notificationTokens", ApplicationToken::class.java) - -val config = FirebaseConfigRegistry.firebaseConfig -val firebaseDatabase = setupFirebaseInstance(config.databaseName, config.configFile) - -val balanceStore = EntityStore(firebaseDatabase, balanceEntity) -val productStore = EntityStore(firebaseDatabase, productEntity) -val subscriptionStore = EntityStore(firebaseDatabase, subscriptionEntity) -val subscriberStore = EntityStore(firebaseDatabase, subscriberEntity) -val paymentHistoryStore = EntityStore(firebaseDatabase, paymentHistoryEntity) -val fcmTokenStore = EntityStore(firebaseDatabase, fcmTokenEntity) - -private fun setupFirebaseInstance( - databaseName: String, - configFile: String): FirebaseDatabase { - - try { - - val credentials: GoogleCredentials = if (Files.exists(Paths.get(configFile))) { - FileInputStream(configFile).use { serviceAccount -> GoogleCredentials.fromStream(serviceAccount) } - } else { - GoogleCredentials.getApplicationDefault() - } - - val options = FirebaseOptions.Builder() - .setCredentials(credentials) - .setDatabaseUrl("https://$databaseName.firebaseio.com/") - .build() - try { - FirebaseApp.getInstance() - } catch (e: Exception) { - FirebaseApp.initializeApp(options) - } - - return FirebaseDatabase.getInstance() - - // (un)comment next line to turn on/of extended debugging - // from firebase. - // this.firebaseDatabase.setLogLevel(com.google.firebase.database.Logger.Level.DEBUG); - } catch (ex: IOException) { - throw StorageException(ex) - } -} +private val config = FirebaseConfigRegistry.firebaseConfig const val TIMEOUT: Long = 10 //sec diff --git a/graph-store/README.md b/graph-store/README.md new file mode 100644 index 000000000..0b00a1c87 --- /dev/null +++ b/graph-store/README.md @@ -0,0 +1,39 @@ +# Using neo4j as Graph datasource + + +`cypher` is SQL like query language, based on ASCII art, to interact with graph database. + +Programmatically, there are different alternatives to interact with neo4j. +They are listed below. + +## JDBI + + Directly use dropwizard-jdbi. + + * This will involve having JDO interfaces, which will have methods, having annotations with cypher queries directly in +their annotations. + * Can handle complex cypher queries, which also means no compile-time checks. + * Duplication of code for normal CRUD operation for most of the entities. + * Ref: + * https://www.dropwizard.io/1.3.2/docs/manual/jdbi3.html + * https://neo4j.com/docs/developer-manual/3.3/cypher/ + +## jCypher + + Java DSL for cypher. + + * Java Fluent DSL equivalent for cypher. + * Ref: https://github.com/Wolfgang-Schuetzelhofer/jcypher/wiki + +## OGM + + Object-Graph-Mapping, which is similar to ORM for relational database. + + * Has annotations like `NodeEntity` & `RelationEntity` for classes, `Relationship` for member collections. + * Ref: https://neo4j.com/docs/ogm-manual/current/tutorial/ + +## Traversal framework Java API + * Ref: https://neo4j.com/docs/java-reference/3.3/tutorial-traversal/ + +## Java Cypher API using Bolt protocol fro Embedded Neo4j + * Ref: https://neo4j.com/docs/java-reference/3.3/tutorials-java-embedded/#tutorials-java-embedded-bolt diff --git a/graph-store/build.gradle b/graph-store/build.gradle new file mode 100644 index 000000000..16594253b --- /dev/null +++ b/graph-store/build.gradle @@ -0,0 +1,30 @@ +plugins { + id "java-library" + id "jacoco" + id "com.github.johnrengelman.shadow" version "2.0.4" + id "org.jetbrains.kotlin.jvm" version "1.2.41" + id "idea" + id "project-report" +} + +ext.neo4jVersion="3.4.0" +ext.neo4jDriverVersion="1.6.1" + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation project(":prime-api") + implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.5" + implementation "com.fasterxml.jackson.core:jackson-databind:2.9.5" + + implementation "org.neo4j:neo4j-graphdb-api:$neo4jVersion" + implementation "org.neo4j.driver:neo4j-java-driver:$neo4jDriverVersion" + + testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlinVersion" + testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion" +} \ No newline at end of file diff --git a/graph-store/src/main/kotlin/org/ostelco/prime/storage/graph/Graph.kt b/graph-store/src/main/kotlin/org/ostelco/prime/storage/graph/Graph.kt new file mode 100644 index 000000000..78ed02418 --- /dev/null +++ b/graph-store/src/main/kotlin/org/ostelco/prime/storage/graph/Graph.kt @@ -0,0 +1,146 @@ +package org.ostelco.prime.storage.graph + +import org.neo4j.graphdb.GraphDatabaseService +import org.neo4j.graphdb.Label +import org.neo4j.graphdb.Node +import org.neo4j.graphdb.Relationship +import org.neo4j.graphdb.RelationshipType +import org.neo4j.graphdb.ResourceIterable +import org.neo4j.graphdb.ResourceIterator +import org.neo4j.graphdb.Result +import org.neo4j.graphdb.Transaction +import org.neo4j.graphdb.event.KernelEventHandler +import org.neo4j.graphdb.event.TransactionEventHandler +import org.neo4j.graphdb.index.IndexManager +import org.neo4j.graphdb.schema.Schema +import org.neo4j.graphdb.traversal.BidirectionalTraversalDescription +import org.neo4j.graphdb.traversal.TraversalDescription +import java.util.concurrent.TimeUnit + +object Graph: GraphDatabaseService { + + override fun createNode(): Node { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun createNode(vararg labels: Label?): Node { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun unregisterTransactionEventHandler(handler: TransactionEventHandler?): TransactionEventHandler { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun index(): IndexManager { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun bidirectionalTraversalDescription(): BidirectionalTraversalDescription { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun registerKernelEventHandler(handler: KernelEventHandler?): KernelEventHandler { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getNodeById(id: Long): Node { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getAllLabels(): ResourceIterable