diff --git a/.gcloudignore b/.gcloudignore new file mode 100644 index 000000000..03b953486 --- /dev/null +++ b/.gcloudignore @@ -0,0 +1,8 @@ +.gcloudignore + +.git +.gitignore + +#!include:.gitignore + +pantel-prod.json \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4c7a9913f..e5ed15a0d 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ out target secrets/* .nb-gradle -.swagger_gen_dir \ No newline at end of file +.swagger_gen_dir diff --git a/.travis.yml b/.travis.yml index a5051417c..79d1f2c5a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,15 +9,23 @@ 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 - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ after_success: - - java -cp ~/codacy-coverage-reporter-assembly-latest.jar com.codacy.CodacyCoverageReporter report -l Java -r prime/build/reports/jacoco/test/jacocoTestReport.xml + - java -cp ~/codacy-coverage-reporter-assembly-latest.jar com.codacy.CodacyCoverageReporter report -l Java -r analytics/build/reports/jacoco/test/jacocoTestReport.xml --partial + - java -cp ~/codacy-coverage-reporter-assembly-latest.jar com.codacy.CodacyCoverageReporter report -l Java -r client-api/build/reports/jacoco/test/jacocoTestReport.xml --partial + - java -cp ~/codacy-coverage-reporter-assembly-latest.jar com.codacy.CodacyCoverageReporter report -l Java -r diameter-stack/build/reports/jacoco/test/jacocoTestReport.xml --partial + - java -cp ~/codacy-coverage-reporter-assembly-latest.jar com.codacy.CodacyCoverageReporter report -l Java -r embedded-graph-store/build/reports/jacoco/test/jacocoTestReport.xml --partial + - java -cp ~/codacy-coverage-reporter-assembly-latest.jar com.codacy.CodacyCoverageReporter report -l Java -r ocs/build/reports/jacoco/test/jacocoTestReport.xml --partial + - java -cp ~/codacy-coverage-reporter-assembly-latest.jar com.codacy.CodacyCoverageReporter report -l Java -r ocsgw/build/reports/jacoco/test/jacocoTestReport.xml --partial + - java -cp ~/codacy-coverage-reporter-assembly-latest.jar com.codacy.CodacyCoverageReporter report -l Java -r ostelco-lib/build/reports/jacoco/test/jacocoTestReport.xml --partial + - java -cp ~/codacy-coverage-reporter-assembly-latest.jar com.codacy.CodacyCoverageReporter report -l Java -r pseudonym-server/build/reports/jacoco/test/jacocoTestReport.xml --partial + - java -cp ~/codacy-coverage-reporter-assembly-latest.jar com.codacy.CodacyCoverageReporter final diff --git a/README.md b/README.md index bf6a3c2cb..772fbc1eb 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) @@ -28,5 +32,8 @@ Mono Repository for core protocols and services around a OCS/BSS for packet data [seagull](./seagull/README.md) -[![Build Status](https://travis-ci.org/ostelco/ostelco-core.svg?branch=master)](https://travis-ci.org/ostelco/ostelco-core) [![codebeat badge](https://codebeat.co/badges/e4c26ba7-75d6-48d2-a3d0-f72988998642)](https://codebeat.co/projects/github-com-ostelco-ostelco-core-master) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/e7b2ae0440104a5e8ae6fa5e919147dc)](https://www.codacy.com/app/la3lma/ostelco-core?utm_source=github.com&utm_medium=referral&utm_content=ostelco/ostelco-core&utm_campaign=Badge_Grade)[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/e7b2ae0440104a5e8ae6fa5e919147dc)](https://www.codacy.com/app/la3lma/ostelco-core?utm_source=github.com&utm_medium=referral&utm_content=ostelco/ostelco-core&utm_campaign=Badge_Coverage) +[![Build Status](https://travis-ci.org/ostelco/ostelco-core.svg?branch=master)](https://travis-ci.org/ostelco/ostelco-core) +[![codebeat badge](https://codebeat.co/badges/e4c26ba7-75d6-48d2-a3d0-f72988998642)](https://codebeat.co/projects/github-com-ostelco-ostelco-core-master) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d15007ecfc2942f7901673177e147d09)](https://www.codacy.com/app/vihang.patil/ostelco-core?utm_source=github.com&utm_medium=referral&utm_content=ostelco/ostelco-core&utm_campaign=Badge_Grade) +[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/d15007ecfc2942f7901673177e147d09)](https://www.codacy.com/app/vihang.patil/ostelco-core?utm_source=github.com&utm_medium=referral&utm_content=ostelco/ostelco-core&utm_campaign=Badge_Coverage) diff --git a/acceptance-tests/build.gradle b/acceptance-tests/build.gradle index 26e9f04ff..bc2d1608d 100644 --- a/acceptance-tests/build.gradle +++ b/acceptance-tests/build.gradle @@ -1,7 +1,7 @@ plugins { + id "org.jetbrains.kotlin.jvm" version "1.2.50" id "application" id "com.github.johnrengelman.shadow" version "2.0.4" - id "org.jetbrains.kotlin.jvm" version "1.2.41" } dependencies { 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..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 @@ -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,23 @@ 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 + } + + 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 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/admin-api/build.gradle b/admin-api/build.gradle index 28e2d9245..b52cd77c3 100644 --- a/admin-api/build.gradle +++ b/admin-api/build.gradle @@ -1,11 +1,9 @@ plugins { - id "java-library" - id "org.jetbrains.kotlin.jvm" version "1.2.41" + id "org.jetbrains.kotlin.jvm" version "1.2.50" + id "java-library" } dependencies { - - implementation project(":prime-api") - - testImplementation "io.dropwizard:dropwizard-testing:$dropwizardVersion" + implementation project(":prime-api") + testImplementation "io.dropwizard:dropwizard-testing:$dropwizardVersion" } \ No newline at end of file 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/analytics/build.gradle b/analytics/build.gradle index cea7f799d..91394d7af 100644 --- a/analytics/build.gradle +++ b/analytics/build.gradle @@ -1,15 +1,14 @@ plugins { + id "org.jetbrains.kotlin.jvm" version "1.2.50" id "application" - id "jacoco" id "com.github.johnrengelman.shadow" version "2.0.4" - id "org.jetbrains.kotlin.jvm" version "1.2.41" id "idea" } dependencies { implementation project(':ocs-api') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" - implementation 'com.google.cloud:google-cloud-pubsub:1.32.0' + implementation "com.google.cloud:google-cloud-pubsub:$googleCloudVersion" implementation 'com.google.cloud.dataflow:google-cloud-dataflow-java-sdk-all:2.4.0' runtimeOnly 'org.apache.beam:beam-runners-google-cloud-dataflow-java:2.4.0' implementation 'ch.qos.logback:logback-classic:1.2.3' @@ -24,14 +23,4 @@ shadowJar { version = null } -jacocoTestReport { - group = "Reporting" - description = "Generate Jacoco coverage reports after running tests." - additionalSourceDirs = files(sourceSets.main.allJava.srcDirs) - reports { - xml.enabled = true - html.enabled = true - } -} - -check.dependsOn jacocoTestReport \ No newline at end of file +apply from: '../jacoco.gradle' \ No newline at end of file diff --git a/app-notifier/build.gradle b/app-notifier/build.gradle index 130ada9fb..e6a8e3267 100644 --- a/app-notifier/build.gradle +++ b/app-notifier/build.gradle @@ -1,10 +1,6 @@ plugins { + id "org.jetbrains.kotlin.jvm" version "1.2.50" 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" } dependencies { 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/auth-server/build.gradle b/auth-server/build.gradle index 885260d7e..a2747d28e 100644 --- a/auth-server/build.gradle +++ b/auth-server/build.gradle @@ -1,8 +1,7 @@ plugins { + id "org.jetbrains.kotlin.jvm" version "1.2.50" id "application" - id "jacoco" id "com.github.johnrengelman.shadow" version "2.0.4" - id "org.jetbrains.kotlin.jvm" version "1.2.41" id "idea" } @@ -57,24 +56,13 @@ configurations { } task integrationTest(type: Test, description: 'Runs the integration tests.', group: 'Verification') { - testClassesDir = sourceSets.integrationTest.output.classesDir + testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath } -check.dependsOn integrationTest integrationTest.mustRunAfter test -jacocoTestReport { - group = "Reporting" - description = "Generate Jacoco coverage reports after running tests." - additionalSourceDirs = files(sourceSets.main.allJava.srcDirs) - reports { - xml.enabled = true - html.enabled = true - } -} - -check.dependsOn jacocoTestReport +apply from: '../jacoco.gradle' idea { module { diff --git a/build.gradle b/build.gradle index 8ec28a86f..26acd226c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,18 @@ +// these are needed only in top-level module plugins { id "java" - id "maven" id "project-report" - id "com.github.ben-manes.versions" version "0.17.0" + id "com.github.ben-manes.versions" version "0.19.0" + id "jacoco" } allprojects { + apply plugin: 'jacoco' group = 'org.ostelco' version = '1.0.0-SNAPSHOT' repositories { - mavenLocal() mavenCentral() jcenter() maven { url = "https://repository.jboss.org/nexus/content/repositories/releases/" } @@ -27,8 +28,10 @@ subprojects { options.encoding = 'UTF-8' } ext { - kotlinVersion = "1.2.41" - dropwizardVersion = "1.3.3" + kotlinVersion = "1.2.50" + dropwizardVersion = "1.3.4" + googleCloudVersion = "1.34.0" + jacksonVersion = "2.9.6" } } diff --git a/client-api/API.md b/client-api/README.md similarity index 98% rename from client-api/API.md rename to client-api/README.md index fd4709029..6ae3974bc 100644 --- a/client-api/API.md +++ b/client-api/README.md @@ -53,6 +53,9 @@ 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/). ## Data model diff --git a/client-api/build.gradle b/client-api/build.gradle index 9f43850cf..914a31e4c 100644 --- a/client-api/build.gradle +++ b/client-api/build.gradle @@ -1,6 +1,6 @@ plugins { + id "org.jetbrains.kotlin.jvm" version "1.2.50" id "java-library" - id "org.jetbrains.kotlin.jvm" version "1.2.41" } dependencies { @@ -17,7 +17,7 @@ dependencies { testImplementation "io.dropwizard:dropwizard-client:$dropwizardVersion" testImplementation "io.dropwizard:dropwizard-testing:$dropwizardVersion" - testImplementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.9.5' + testImplementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion" testImplementation "org.mockito:mockito-core:2.18.3" testImplementation 'org.assertj:assertj-core:3.10.0' @@ -37,4 +37,6 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { jvmTarget = "1.8" } -} \ No newline at end of file +} + +apply from: '../jacoco.gradle' \ No newline at end of file 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..b9e902383 --- /dev/null +++ b/client-api/src/main/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResource.kt @@ -0,0 +1,48 @@ +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 result = dao.getMsisdn(authToken.name) + + 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(507) // Insufficient Storage + .entity(asJson(created.left().get())) + .build() + } + } else { + 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/SubscriberDAO.kt b/client-api/src/main/kotlin/org/ostelco/prime/client/api/store/SubscriberDAO.kt index 21951565c..5be0cf61e 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(msisdn: String, applicationToken: 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..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 @@ -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 @@ -49,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, @@ -64,12 +65,33 @@ class SubscriberDAOImpl(private val storage: Storage, private val ocsSubscriberS return getProfile(subscriptionId) } + 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 { if (!SubscriberDAO.isValidProfile(profile)) { return Either.left(ApiError("Incomplete profile description")) } try { - storage.updateSubscriber(subscriptionId, Subscriber( + storage.updateSubscriber(Subscriber( profile.email, profile.name, profile.address, @@ -95,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 { @@ -114,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")) } @@ -131,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) } @@ -142,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/client-api/src/test/kotlin/org/ostelco/prime/client/api/auth/GetUserInfoTest.kt b/client-api/src/test/kotlin/org/ostelco/prime/client/api/auth/GetUserInfoTest.kt index a063a4479..684a54e44 100644 --- a/client-api/src/test/kotlin/org/ostelco/prime/client/api/auth/GetUserInfoTest.kt +++ b/client-api/src/test/kotlin/org/ostelco/prime/client/api/auth/GetUserInfoTest.kt @@ -7,6 +7,7 @@ import io.dropwizard.testing.junit.DropwizardAppRule import io.vavr.collection.Array import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.fail +import org.glassfish.jersey.client.ClientProperties import org.junit.BeforeClass import org.junit.ClassRule import org.junit.Test @@ -43,6 +44,8 @@ class GetUserInfoTest { val response = client!!.target( "http://localhost:${RULE.localPort}/profile") .request() + .property(ClientProperties.CONNECT_TIMEOUT, 30000) + .property(ClientProperties.READ_TIMEOUT, 30000) .header("Authorization", "Bearer ${AccessToken.withEmail(email, audience)}") .get(Response::class.java) @@ -59,6 +62,7 @@ class GetUserInfoTest { .request() .get(Response::class.java) if (r.status == 200) { + println("Connected") break } } catch (t: Throwable) { @@ -86,7 +90,7 @@ class GetUserInfoTest { @BeforeClass @JvmStatic fun setUpClient() { - client = JerseyClientBuilder(RULE.environment).build("test client") + client = JerseyClientBuilder(RULE.environment).build("test client"); } } } 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..a0e83daed --- /dev/null +++ b/client-api/src/test/kotlin/org/ostelco/prime/client/api/resources/ApplicationTokenResourceTest.kt @@ -0,0 +1,100 @@ +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 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 + +/** + * 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" + + private val applicationToken = ApplicationToken() + + @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() + + 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) + .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(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 client: Client = mock(Client::class.java) + + @JvmField + @ClassRule + val RULE = ResourceTestRule.builder() + .addResource(AuthDynamicFeature( + OAuthCredentialAuthFilter.Builder() + .setAuthenticator(AUTHENTICATOR) + .setPrefix("Bearer") + .buildAuthFilter())) + .addResource(AuthValueFactoryProvider.Binder(AccessTokenPrincipal::class.java)) + .addResource(ApplicationTokenResource(DAO)) + .setTestContainerFactory(GrizzlyWebTestContainerFactory()) + .build() + } +} diff --git a/diameter-stack/build.gradle b/diameter-stack/build.gradle index 32b36e31f..ec5b9d87a 100644 --- a/diameter-stack/build.gradle +++ b/diameter-stack/build.gradle @@ -1,6 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.2.41" - id "idea" + id "org.jetbrains.kotlin.jvm" version "1.2.50" id "java-library" id "signing" id "maven" @@ -93,4 +92,6 @@ uploadArchives.repositories.mavenDeployer { } } } -} \ No newline at end of file +} + +apply from: '../jacoco.gradle' \ No newline at end of file diff --git a/diameter-test/build.gradle b/diameter-test/build.gradle index 1fc142c91..7b50903bd 100644 --- a/diameter-test/build.gradle +++ b/diameter-test/build.gradle @@ -1,6 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.2.41" - id "idea" + id "org.jetbrains.kotlin.jvm" version "1.2.50" id "java-library" id "signing" id "maven" diff --git a/docker-compose.override.yaml b/docker-compose.override.yaml index fd6c88b4e..c434fcb42 100644 --- a/docker-compose.override.yaml +++ b/docker-compose.override.yaml @@ -18,13 +18,33 @@ services: net: aliases: - "prime" + ipv4_address: 172.16.238.5 + default: + + esp: + container_name: esp + image: gcr.io/endpoints-release/endpoints-runtime:1 + volumes: + - "./prime/config:/esp" + - "./esp:/etc/nginx/ssl" + command: > + --service=ocs.endpoints.pantel-2decb.cloud.goog + --rollout_strategy=managed + --http2_port=80 + --ssl_port=443 + --backend=grpc://172.16.238.5:8082 + --service_account_key=/esp/pantel-prod.json + networks: + net: + aliases: + - "ocs.endpoints.pantel-2decb.cloud.goog" ipv4_address: 172.16.238.4 default: ocsgw: depends_on: - "prime" - command: ["./wait_for_prime.sh"] + command: ["./wait_for_esp_and_prime.sh"] networks: net: aliases: @@ -43,6 +63,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/docker-compose.prod.yaml b/docker-compose.prod.yaml index a2da90266..4e9b95958 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -2,11 +2,4 @@ version: "3.3" services: ocsgw: - environment: - - GOOGLE_APPLICATION_CREDENTIALS=/config/pantel-prod.json - # This is external IP from "kubectl get service" - - GRPC_SERVER=ocs.endpoints.pantel-2decb.cloud.goog - - GRPC_ENCRYPTION=true - extra_hosts: - - "ocs.endpoints.pantel-2decb.cloud.goog:35.195.49.238" - network_mode: "host" + network_mode: "host" \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 19f06dbb7..906809096 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,6 +4,10 @@ services: ocsgw: container_name: ocsgw build: ocsgw + environment: + - GOOGLE_APPLICATION_CREDENTIALS=/config/pantel-prod.json + - GRPC_SERVER=ocs.endpoints.pantel-2decb.cloud.goog + - GRPC_ENCRYPTION=true auth-server: container_name: auth-server diff --git a/docs/LOGS.md b/docs/LOGS.md index e4c2fb7b2..f0a361237 100644 --- a/docs/LOGS.md +++ b/docs/LOGS.md @@ -21,5 +21,3 @@ * Goto [this link](https://console.cloud.google.com/logs/viewer?project=pantel-2decb) * GKE container > private-cluster > All namespace_id * You can expand a single log and filter to log prime-only logs. - - 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 4552eee32..89e7eced1 100644 --- a/docs/TEST.md +++ b/docs/TEST.md @@ -2,9 +2,12 @@ ### 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`. + * 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 with default balance by importing `docs/pantel-2decb_test.json` at `/test` path in Firebase. diff --git a/embedded-graph-store/build.gradle b/embedded-graph-store/build.gradle new file mode 100644 index 000000000..e86d1aca9 --- /dev/null +++ b/embedded-graph-store/build.gradle @@ -0,0 +1,26 @@ +plugins { + id "org.jetbrains.kotlin.jvm" version "1.2.50" + id "java-library" +} + +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:$jacksonVersion" + implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + + implementation "org.neo4j:neo4j-bolt:$neo4jVersion" + + testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlinVersion" + testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion" +} + +apply from: '../jacoco.gradle' \ 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/esp/.gitignore b/esp/.gitignore new file mode 100644 index 000000000..47c805dfe --- /dev/null +++ b/esp/.gitignore @@ -0,0 +1,2 @@ +nginx.crt +nginx.key \ No newline at end of file diff --git a/ext-auth-provider/build.gradle b/ext-auth-provider/build.gradle index 93a83c51d..d2fc0cbab 100644 --- a/ext-auth-provider/build.gradle +++ b/ext-auth-provider/build.gradle @@ -1,10 +1,7 @@ plugins { + id "org.jetbrains.kotlin.jvm" version "1.2.50" id "application" - 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" } dependencies { diff --git a/firebase-store/build.gradle b/firebase-store/build.gradle index fbc2b4e14..3b1e86cbd 100644 --- a/firebase-store/build.gradle +++ b/firebase-store/build.gradle @@ -1,10 +1,6 @@ plugins { + id "org.jetbrains.kotlin.jvm" version "1.2.50" 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" } dependencies { 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 b547ddef3..0e3f04bb4 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 @@ -31,10 +32,57 @@ class FirebaseStorage : Storage by FirebaseStorageSingleton object FirebaseStorageSingleton : Storage { + private val LOG by logger() + + 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() + + } catch (ex: IOException) { + throw StorageException(ex) + } + } + override val balances: Map get() = balanceStore.getAll() - override fun addSubscriber(id: String, subscriber: Subscriber) = 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) @@ -42,27 +90,28 @@ 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 return balanceStore.get(msisdn) } - override fun setBalance(msisdn: String, noOfBytes: Long) = balanceStore.update(msisdn, noOfBytes) + override fun setBalance(msisdn: String, noOfBytes: Long):Boolean = balanceStore.update(msisdn, noOfBytes) override fun getPurchaseRecords(id: String): Collection { return paymentHistoryStore.getAll { @@ -78,75 +127,36 @@ 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: String) { - fcmTokenStore.create(msisdn, token) + override fun addNotificationToken(msisdn: String, token: ApplicationToken) : Boolean { + return fcmTokenStore.set(token.applicationID, token) { databaseReference.child(urlEncode(msisdn)) } } - override fun getNotificationToken(msisdn: String): String? { - return fcmTokenStore.get(msisdn) + override fun getNotificationToken(msisdn: String, applicationID: String): ApplicationToken? { + return fcmTokenStore.get(applicationID) { databaseReference.child(msisdn) } } -} -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/FCM", String::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) + override fun getNotificationTokens(msisdn: String): Collection { + return fcmTokenStore.getAll { + databaseReference.child(urlEncode(msisdn)) + }.values } } +private val config = FirebaseConfigRegistry.firebaseConfig + const val TIMEOUT: Long = 10 //sec class EntityType( @@ -167,10 +177,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() @@ -245,6 +255,7 @@ class EntityStore( fun create(id: String, entity: E): Boolean { // fail if already exist if (exists(id)) { + LOG.warn("Failed to create. id {} already exists", id) return false } return set(id, entity) @@ -264,8 +275,8 @@ class EntityStore( fun add(entity: E, reference: EntityStore.() -> DatabaseReference = { databaseReference }): String? { val newPushedEntry = reference().push() val future = newPushedEntry.setValueAsync(entity) - future.get(TIMEOUT, SECONDS) ?: return null - return newPushedEntry.key + future.get(TIMEOUT, SECONDS) + return if (future.isDone) newPushedEntry.key else null } /** @@ -275,6 +286,7 @@ class EntityStore( */ fun update(id: String, entity: E): Boolean { if (dontExists(id)) { + LOG.warn("Failed to update. id {} does not exists", id) return false } return set(id, entity) @@ -285,10 +297,10 @@ class EntityStore( * * @return success */ - fun set(id: String, entity: E): Boolean { - val future = databaseReference.child(urlEncode(id)).setValueAsync(entity) - future.get(TIMEOUT, SECONDS) ?: return false - return true + fun set(id: String, entity: E, reference: EntityStore.() -> DatabaseReference = { databaseReference }): Boolean { + val future = reference().child(urlEncode(id)).setValueAsync(entity) + future.get(TIMEOUT, SECONDS) + return future.isDone } /** @@ -304,7 +316,7 @@ class EntityStore( return false } val future = databaseReference.child(urlEncode(id)).removeValueAsync() - future.get(TIMEOUT, SECONDS) ?: return false - return true + future.get(TIMEOUT, SECONDS) + return future.isDone } } \ No newline at end of file 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..cce9042c8 --- /dev/null +++ b/graph-store/build.gradle @@ -0,0 +1,26 @@ +plugins { + id "org.jetbrains.kotlin.jvm" version "1.2.50" + id "java-library" +} + +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:$jacksonVersion" + implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + + 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