├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ └── maven-wrapper.properties ├── .travis.yml ├── .travis_after_success.sh ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── build.sh ├── config.yml ├── maven_deploy_settings.xml ├── mvnw ├── mvnw.cmd ├── notification-api ├── LICENSE ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── smoketurner │ │ └── notification │ │ └── api │ │ ├── Notification.java │ │ └── Rule.java │ └── test │ ├── java │ └── com │ │ └── smoketurner │ │ └── notification │ │ └── api │ │ ├── NotificationTest.java │ │ └── RuleTest.java │ └── resources │ └── fixtures │ ├── notification.json │ ├── rule.json │ ├── rule_invalid_duration.json │ ├── rule_invalid_matchon.json │ └── rule_invalid_timeunit.json ├── notification-application ├── LICENSE ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── smoketurner │ │ │ └── notification │ │ │ └── application │ │ │ ├── NotificationApplication.java │ │ │ ├── config │ │ │ └── NotificationConfiguration.java │ │ │ ├── core │ │ │ ├── IdGenerator.java │ │ │ ├── Matcher.java │ │ │ ├── RangeHeader.java │ │ │ ├── Rollup.java │ │ │ ├── StringSetParam.java │ │ │ ├── UserNotifications.java │ │ │ └── WebSecurityFilter.java │ │ │ ├── exceptions │ │ │ ├── NotificationException.java │ │ │ ├── NotificationExceptionMapper.java │ │ │ └── NotificationStoreException.java │ │ │ ├── graphql │ │ │ ├── CreateNotificationMutation.java │ │ │ ├── CreateRuleMutation.java │ │ │ ├── NotificationDataFetcher.java │ │ │ ├── RemoveAllNotificationsMutation.java │ │ │ ├── RemoveAllRulesMutation.java │ │ │ ├── RemoveNotificationMutation.java │ │ │ ├── RemoveRuleMutation.java │ │ │ ├── RuleDataFetcher.java │ │ │ ├── Scalars.java │ │ │ └── UsernameFieldValidation.java │ │ │ ├── managed │ │ │ ├── CursorStoreManager.java │ │ │ └── NotificationStoreManager.java │ │ │ ├── resources │ │ │ ├── NotificationResource.java │ │ │ ├── PingResource.java │ │ │ ├── RuleResource.java │ │ │ └── VersionResource.java │ │ │ ├── riak │ │ │ ├── CursorObject.java │ │ │ ├── CursorResolver.java │ │ │ ├── CursorUpdate.java │ │ │ ├── NotificationListAddition.java │ │ │ ├── NotificationListConverter.java │ │ │ ├── NotificationListDeletion.java │ │ │ ├── NotificationListObject.java │ │ │ └── NotificationListResolver.java │ │ │ └── store │ │ │ ├── CursorStore.java │ │ │ ├── NotificationStore.java │ │ │ └── RuleStore.java │ ├── proto │ │ └── notification.proto │ └── resources │ │ ├── Notification.graphql │ │ └── banner.txt │ └── test │ ├── java │ └── com │ │ └── smoketurner │ │ └── notification │ │ └── application │ │ ├── NotificationApplicationTest.java │ │ ├── benchmarks │ │ ├── NotificationStoreBenchmark.java │ │ └── RollupBenchmark.java │ │ ├── core │ │ ├── MatcherTest.java │ │ ├── RangeHeaderTest.java │ │ ├── RollupTest.java │ │ ├── StringSetParamTest.java │ │ └── UserNotificationsTest.java │ │ ├── graphql │ │ ├── CreateNotificationMutationTest.java │ │ ├── CreateRuleMutationTest.java │ │ ├── NotificationDataFetcherTest.java │ │ ├── RemoveAllNotificationsMutationTest.java │ │ ├── RemoveAllRulesMutationTest.java │ │ ├── RemoveNotificationMutationTest.java │ │ ├── RemoveRuleMutationTest.java │ │ ├── RuleDataFetcherTest.java │ │ └── UsernameFieldValidationTest.java │ │ ├── integration │ │ └── NotificationsIT.java │ │ ├── managed │ │ ├── CursorStoreManagerTest.java │ │ └── NotificationStoreManagerTest.java │ │ ├── resources │ │ ├── NotificationResourceTest.java │ │ ├── PingResourceTest.java │ │ ├── RuleResourceTest.java │ │ └── VersionResourceTest.java │ │ ├── riak │ │ ├── CursorObjectTest.java │ │ ├── CursorResolverTest.java │ │ ├── CursorUpdateTest.java │ │ ├── NotificationListAdditionTest.java │ │ ├── NotificationListConverterTest.java │ │ ├── NotificationListDeletionTest.java │ │ ├── NotificationListObjectTest.java │ │ └── NotificationListResolverTest.java │ │ └── store │ │ ├── CursorStoreTest.java │ │ └── NotificationStoreTest.java │ └── resources │ ├── Notification.graphql │ ├── fixtures │ └── cursor.json │ ├── logback-test.xml │ └── notification-test.yml ├── notification-client ├── LICENSE ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── smoketurner │ │ └── notification │ │ └── client │ │ ├── NotificationClient.java │ │ ├── NotificationClientBuilder.java │ │ └── NotificationClientConfiguration.java │ └── test │ ├── java │ └── com │ │ └── smoketurner │ │ └── notification │ │ └── client │ │ ├── NotificationClientTest.java │ │ └── integration │ │ └── NotificationGenerator.java │ └── resources │ └── logback-test.xml ├── pom.xml ├── riak_schemas └── maps.dt ├── run.sh ├── spotbugs.xml └── start_riak.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | .gitattributes 4 | .gitignore 5 | .java-version 6 | .travis.yml 7 | .travis_after_success.sh 8 | .checkstyle.xml 9 | .project 10 | .classpath 11 | .settings 12 | target 13 | *.class 14 | CONTRIBUTING.md 15 | maven_deploy_settings.xml 16 | Procfile 17 | setup.sh 18 | system.properties 19 | Dockerfile 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Configuration file for EditorConfig: http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.properties] 13 | charset = latin1 14 | 15 | [travis.yml] 16 | indent_size = 2 17 | indent_style = space 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text auto 2 | 3 | *.java text eol=lf 4 | *.yml text eol=lf 5 | *.xml text eol=lf 6 | *.cmd text eol=lf 7 | *.md text eol=lf 8 | *.sh text eol=lf 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | .idea 5 | *.iml 6 | .DS_Store 7 | *.class 8 | *.jar 9 | hs_err_pid* 10 | target 11 | dependency-reduced-pom.xml 12 | .apt_generated_tests 13 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | import java.net.*; 21 | import java.io.*; 22 | import java.nio.channels.*; 23 | import java.util.Properties; 24 | 25 | public class MavenWrapperDownloader { 26 | 27 | /** 28 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 29 | */ 30 | private static final String DEFAULT_DOWNLOAD_URL = 31 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; 32 | 33 | /** 34 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 35 | * use instead of the default one. 36 | */ 37 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 38 | ".mvn/wrapper/maven-wrapper.properties"; 39 | 40 | /** 41 | * Path where the maven-wrapper.jar will be saved to. 42 | */ 43 | private static final String MAVEN_WRAPPER_JAR_PATH = 44 | ".mvn/wrapper/maven-wrapper.jar"; 45 | 46 | /** 47 | * Name of the property which should be used to override the default download url for the wrapper. 48 | */ 49 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 50 | 51 | public static void main(String args[]) { 52 | System.out.println("- Downloader started"); 53 | File baseDirectory = new File(args[0]); 54 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 55 | 56 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 57 | // wrapperUrl parameter. 58 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 59 | String url = DEFAULT_DOWNLOAD_URL; 60 | if(mavenWrapperPropertyFile.exists()) { 61 | FileInputStream mavenWrapperPropertyFileInputStream = null; 62 | try { 63 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 64 | Properties mavenWrapperProperties = new Properties(); 65 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 66 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 67 | } catch (IOException e) { 68 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 69 | } finally { 70 | try { 71 | if(mavenWrapperPropertyFileInputStream != null) { 72 | mavenWrapperPropertyFileInputStream.close(); 73 | } 74 | } catch (IOException e) { 75 | // Ignore ... 76 | } 77 | } 78 | } 79 | System.out.println("- Downloading from: : " + url); 80 | 81 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 82 | if(!outputFile.getParentFile().exists()) { 83 | if(!outputFile.getParentFile().mkdirs()) { 84 | System.out.println( 85 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 86 | } 87 | } 88 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 89 | try { 90 | downloadFileFromURL(url, outputFile); 91 | System.out.println("Done"); 92 | System.exit(0); 93 | } catch (Throwable e) { 94 | System.out.println("- Error downloading"); 95 | e.printStackTrace(); 96 | System.exit(1); 97 | } 98 | } 99 | 100 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 101 | URL website = new URL(urlString); 102 | ReadableByteChannel rbc; 103 | rbc = Channels.newChannel(website.openStream()); 104 | FileOutputStream fos = new FileOutputStream(destination); 105 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 106 | fos.close(); 107 | rbc.close(); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | os: 3 | - linux 4 | jdk: 5 | - openjdk11 6 | before_install: 7 | - rm ~/.m2/settings.xml || true 8 | - ulimit -c unlimited -S 9 | - mvn -N io.takari:maven:wrapper 10 | - sudo riak-admin bucket-type create maps '{"props":{"datatype":"map"}}' 11 | - sudo riak-admin bucket-type activate maps 12 | after_success: 13 | - bash .travis_after_success.sh 14 | cache: 15 | directories: 16 | - $HOME/.m2 17 | 18 | services: 19 | - riak 20 | 21 | notifications: 22 | email: false 23 | 24 | env: 25 | global: 26 | # CI_DEPLOY_USERNAME 27 | - secure: "S8HDSF/GUdzEt9ZVxr0zVYJSNt2uMXbE3xV8vCV8j40ri9/0z0ZJfvcZx7p6k7sscxhpRsX9KPzYvQwM9ex2O/Aw190XATDiXHbJ0jzCii0agzTSlolJ/HiGCkYzSt7rPbjzB2NKmww2qMeMvzyEoWRFV9P5171Jsm99w+qmVhpwWBogPr9FD5ayRZF78BDoU96ChbLbJxOzjg1N4IaJ6KmAbGxsQ9MLyhV+iJjRIWe9DgR8dd6ja+J4RHF+HfqYg8NJMfg7JGpTO1ULbZHoqaO5e++FkIrn/Srp/qRwfEZxWs4TxTVTMxfKQ6ss8mduOVp1nahu6OC+47pJG3MWZsdU1CDCdjbQ/CrLcVduQtS2+TZoiwdwgyYJFiSim1HWivrNGuhk4ySvonmXVZzuWXMUqAHmbq2nq4/a0HrwfF+mth5B/wgOIuYMwtwP7ibUsunc9+Rx/c+gL3X/Fr49oHvIOySkbQ6FKK23H0MF+lNEUfuqpRREOkbIyhO0sEItCIsoaeDdOw9JqicTiYodE+OkLT3c2oYN+PNtUVV8ZSxdAI5bolArwoVPMxNhSBLPqJIAHfjmAAzGbmttNGrbuARsG3xbFaQcDMDdAfxKsQLMiY2EHajMdQ6ShswMbgGWvIbsksPzYIUJw4FBWuQZcXWt/MIF86ACthPBpBxEiJc=" 28 | # CI_DEPLOY_PASSWORD 29 | - secure: "WcOQLsXDzayW9bbU3mlte9vD7+gd5UmSELR8Sus+HUAdl7DphhQHS0obWNPcJJNcRCDvjoDRUFVaP+KVvAAPwIrBu4HMjhIuAJkiLu3QF382rHTSwKvZvzUA99XJPautoqmdzZofjsdMwOoQZZ/Q32kjc3qqR3MYzs9PtzWt0/oM6YNW590r+cks9MXIZ1K5UPaEIRGO6Q+IMDRzgsq5UvNtLWFAUqlU+xY5xCgubOnkGIUZfMrm1M+Jl4FJg7rT2DOuBVvMjPPlzjjAQXH3HZG/i9ET+2A/zDtxepl2QqJhc/ZhBLLywSdAhQhYIhaa0zpscv7B5e/iqbjR+1TVUimxqWjvasUVqi969iWVyTnw30LeJwuOH/DmJQ/DPguENTWMFBrl4YNSaty4L3heLsF87kJ+4mHOmuAbGEyyCSwLmAFBWRsaLH0gb5f/ng7TpElZ9FrOn+njT1Sg4t5+J7ftTPp9CujET7F6UzfD0aJU10P3qsuU1E7r9YJoaAGWdbJ4N4fKHRkuwd3EJyc0KCKaVHVHZ890tyvzrveDXHon0Mh7/Yv/lBWJ0hdvFtHTOmF1zOUjkKPdJFGTB8pKSthY9+Qlruqym9CDMc0icFB4ciwMJQaP4d4QLrHWBiRlGA2qG4mJKnAcJMyYZN2VF6V8YOiCMk9hJ06d03Aq0q8=" 30 | - JAVA_OPTS: "'-Xms512m -Xmx2g'" 31 | -------------------------------------------------------------------------------- /.travis_after_success.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "${TRAVIS_JDK_VERSION}" != "openjdk11" ]]; then 4 | echo "Skipping after_success actions for JDK version \"${TRAVIS_JDK_VERSION}\"" 5 | exit 6 | fi 7 | 8 | ./mvnw -B cobertura:cobertura coveralls:report 9 | 10 | if [[ -n ${TRAVIS_TAG} ]]; then 11 | echo "Skipping deployment for tag \"${TRAVIS_TAG}\"" 12 | exit 13 | fi 14 | 15 | if [[ ${TRAVIS_BRANCH} != 'master' ]]; then 16 | echo "Skipping deployment for branch \"${TRAVIS_BRANCH}\"" 17 | exit 18 | fi 19 | 20 | if [[ "$TRAVIS_PULL_REQUEST" = "true" ]]; then 21 | echo "Skipping deployment for pull request" 22 | exit 23 | fi 24 | 25 | ./mvnw -B deploy --settings maven_deploy_settings.xml -Dmaven.test.skip=true -Dfindbugs.skip=true 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Guidelines for contributing to Notification. 2 | 3 | Please create a pull request against `master`. 4 | 5 | Try to make the code in the pull request as focused and clean as possible. See 6 | our [style guide](http://google.github.io/styleguide/javaguide.html). If the pull 7 | request is too large, we may ask you to split it into smaller ones. 8 | 9 | # License 10 | By contributing your code, you agree to license your contribution under the 11 | terms of the Apache Public License v2: 12 | https://github.com/smoketurner/notification/blob/master/LICENSE 13 | 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | FROM openjdk:11-jdk-slim AS BUILD_IMAGE 18 | 19 | WORKDIR /app 20 | 21 | RUN mkdir -p notification-api notification-application notification-client 22 | 23 | COPY pom.xml mvnw ./ 24 | COPY .mvn ./.mvn/ 25 | COPY notification-api/pom.xml ./notification-api/ 26 | COPY notification-application/pom.xml ./notification-application/ 27 | COPY notification-client/pom.xml ./notification-client/ 28 | 29 | RUN ./mvnw install 30 | 31 | COPY . . 32 | 33 | RUN ./mvnw clean package -DskipTests=true -Dmaven.javadoc.skip=true -Dmaven.source.skip=true && \ 34 | rm notification-application/target/original-*.jar && \ 35 | mv notification-application/target/*.jar app.jar 36 | 37 | FROM openjdk:11-jre-slim 38 | 39 | ARG VERSION="1.3.1-SNAPSHOT" 40 | 41 | LABEL name="notification" version=$VERSION 42 | 43 | ENV PORT 8080 44 | 45 | RUN apk add --no-cache curl 46 | 47 | WORKDIR /app 48 | COPY --from=BUILD_IMAGE /app/app.jar . 49 | COPY --from=BUILD_IMAGE /app/config.yml . 50 | 51 | HEALTHCHECK --interval=10s --timeout=5s CMD curl --fail http://127.0.0.1:8080/admin/healthcheck || exit 1 52 | 53 | EXPOSE 8080 54 | 55 | ENTRYPOINT ["java", "-d64", "-server", "-jar", "app.jar"] 56 | CMD ["server", "config.yml"] 57 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VERSION=`xmllint --xpath "//*[local-name()='project']/*[local-name()='version']/text()" pom.xml` 4 | 5 | docker build --build-arg VERSION=${VERSION} -t "smoketurner/notification:${VERSION}" . 6 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # GraphQL-specific options. 18 | graphql: 19 | 20 | enableTracing: false 21 | queryCache: maximumSize=10000 22 | schemaFiles: 23 | - Notification.graphql 24 | 25 | # Riak-specific options. 26 | riak: 27 | 28 | nodes: 29 | #- 127.0.0.1:8087 30 | - 10.0.57.33:8087 31 | - 10.0.57.34:8087 32 | - 10.0.57.35:8087 33 | - 10.0.57.36:8087 34 | - 10.0.57.37:8087 35 | 36 | # HTTP-specific options. 37 | server: 38 | 39 | type: simple 40 | rootPath: /api/ 41 | applicationContextPath: / 42 | connector: 43 | type: http 44 | port: ${PORT:-8080} 45 | 46 | requestLog: 47 | appenders: 48 | - type: console 49 | timeZone: UTC 50 | target: stdout 51 | 52 | logging: 53 | level: INFO 54 | loggers: 55 | com.smoketurner.notification: DEBUG 56 | com.basho.riak: INFO 57 | appenders: 58 | - type: console 59 | timeZone: UTC 60 | target: stdout 61 | -------------------------------------------------------------------------------- /maven_deploy_settings.xml: -------------------------------------------------------------------------------- 1 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | sonatype-nexus-snapshots 30 | ${env.CI_DEPLOY_USERNAME} 31 | ${env.CI_DEPLOY_PASSWORD} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /notification-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 4.0.0 21 | 22 | 23 | com.smoketurner.notification 24 | notification-parent 25 | 2.0.1-SNAPSHOT 26 | 27 | 28 | notification-api 29 | Notification API 30 | 31 | 32 | 33 | io.dropwizard 34 | dropwizard-jackson 35 | 36 | 37 | io.dropwizard 38 | dropwizard-validation 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /notification-api/src/main/java/com/smoketurner/notification/api/Rule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonCreator; 19 | import com.fasterxml.jackson.annotation.JsonIgnore; 20 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 21 | import com.fasterxml.jackson.annotation.JsonInclude; 22 | import com.fasterxml.jackson.annotation.JsonProperty; 23 | import io.dropwizard.jackson.JsonSnakeCase; 24 | import io.dropwizard.util.Duration; 25 | import java.util.Objects; 26 | import java.util.Optional; 27 | import java.util.StringJoiner; 28 | import javax.annotation.Nullable; 29 | 30 | @JsonSnakeCase 31 | @JsonIgnoreProperties(ignoreUnknown = true) 32 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 33 | public final class Rule { 34 | 35 | public static final String MAX_SIZE = "max_size"; 36 | public static final String MAX_DURATION = "max_duration"; 37 | public static final String MATCH_ON = "match_on"; 38 | 39 | private final Optional maxSize; 40 | private final Optional maxDuration; 41 | private final Optional matchOn; 42 | 43 | /** 44 | * Constructor 45 | * 46 | * @param maxSize Maximum number of notifications to include in a roll-up 47 | * @param maxDuration Maximum time duration between the first and last notifications in a roll-up 48 | * @param matchOn Group notifications by a specific category 49 | */ 50 | @JsonCreator 51 | private Rule( 52 | @JsonProperty(MAX_SIZE) @Nullable final Integer maxSize, 53 | @JsonProperty(MAX_DURATION) @Nullable final Duration maxDuration, 54 | @JsonProperty(MATCH_ON) @Nullable final String matchOn) { 55 | this.maxSize = Optional.ofNullable(maxSize); 56 | this.maxDuration = Optional.ofNullable(maxDuration); 57 | if (matchOn != null && !matchOn.isEmpty()) { 58 | this.matchOn = Optional.of(matchOn); 59 | } else { 60 | this.matchOn = Optional.empty(); 61 | } 62 | } 63 | 64 | public static Builder builder() { 65 | return new Builder(); 66 | } 67 | 68 | public static class Builder { 69 | @Nullable private Integer maxSize; 70 | 71 | @Nullable private Duration maxDuration; 72 | 73 | @Nullable private String matchOn; 74 | 75 | public Builder withMaxSize(@Nullable final Integer maxSize) { 76 | this.maxSize = maxSize; 77 | return this; 78 | } 79 | 80 | public Builder withMaxDuration(@Nullable final Duration maxDuration) { 81 | this.maxDuration = maxDuration; 82 | return this; 83 | } 84 | 85 | public Builder withMatchOn(@Nullable final String matchOn) { 86 | if (matchOn != null && matchOn.isEmpty()) { 87 | this.matchOn = null; 88 | } else { 89 | this.matchOn = matchOn; 90 | } 91 | return this; 92 | } 93 | 94 | public Rule build() { 95 | return new Rule(maxSize, maxDuration, matchOn); 96 | } 97 | } 98 | 99 | @JsonProperty(MAX_SIZE) 100 | public Optional getMaxSize() { 101 | return maxSize; 102 | } 103 | 104 | @JsonProperty(MAX_DURATION) 105 | public Optional getMaxDuration() { 106 | return maxDuration; 107 | } 108 | 109 | @JsonProperty(MATCH_ON) 110 | public Optional getMatchOn() { 111 | return matchOn; 112 | } 113 | 114 | @JsonIgnore 115 | public boolean isValid() { 116 | return maxSize.isPresent() || maxDuration.isPresent() || matchOn.isPresent(); 117 | } 118 | 119 | @Override 120 | public boolean equals(final Object obj) { 121 | if (this == obj) { 122 | return true; 123 | } 124 | if ((obj == null) || (getClass() != obj.getClass())) { 125 | return false; 126 | } 127 | 128 | final Rule other = (Rule) obj; 129 | return Objects.equals(maxSize, other.maxSize) 130 | && Objects.equals(maxDuration, other.maxDuration) 131 | && Objects.equals(matchOn, other.matchOn); 132 | } 133 | 134 | @Override 135 | public int hashCode() { 136 | return Objects.hash(maxSize, maxDuration, matchOn); 137 | } 138 | 139 | @Override 140 | public String toString() { 141 | return new StringJoiner(", ", Rule.class.getSimpleName() + "{", "}") 142 | .add("maxSize=" + maxSize) 143 | .add("maxDuration=" + maxDuration) 144 | .add("matchOn=" + matchOn) 145 | .toString(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /notification-api/src/test/java/com/smoketurner/notification/api/NotificationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.api; 17 | 18 | import static io.dropwizard.testing.FixtureHelpers.fixture; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | 21 | import com.fasterxml.jackson.databind.ObjectMapper; 22 | import com.google.common.collect.ImmutableList; 23 | import com.google.common.collect.ImmutableMap; 24 | import com.google.common.collect.Sets; 25 | import io.dropwizard.jackson.Jackson; 26 | import java.time.ZonedDateTime; 27 | import java.util.TreeSet; 28 | import org.junit.Before; 29 | import org.junit.Test; 30 | 31 | public class NotificationTest { 32 | private final ObjectMapper MAPPER = Jackson.newObjectMapper(); 33 | private Notification notification; 34 | 35 | @Before 36 | public void setUp() throws Exception { 37 | final Notification notification2 = 38 | Notification.builder("new-follower", "you have a new follower") 39 | .withId("12346") 40 | .withCreatedAt(ZonedDateTime.parse("2015-06-29T21:04:12Z")) 41 | .withUnseen(true) 42 | .withProperties(ImmutableMap.of("first_name", "Test 2", "last_name", "User 2")) 43 | .build(); 44 | 45 | notification = 46 | Notification.builder("new-follower", "you have a new follower") 47 | .withId("12345") 48 | .withCreatedAt(ZonedDateTime.parse("2015-06-29T21:04:12Z")) 49 | .withUnseen(true) 50 | .withProperties(ImmutableMap.of("first_name", "Test", "last_name", "User")) 51 | .withNotifications(ImmutableList.of(notification2)) 52 | .build(); 53 | } 54 | 55 | @Test 56 | public void serializesToJSON() throws Exception { 57 | final String actual = MAPPER.writeValueAsString(notification); 58 | final String expected = 59 | MAPPER.writeValueAsString( 60 | MAPPER.readValue(fixture("fixtures/notification.json"), Notification.class)); 61 | assertThat(actual).isEqualTo(expected); 62 | } 63 | 64 | @Test 65 | public void deserializesFromJSON() throws Exception { 66 | final Notification actual = 67 | MAPPER.readValue(fixture("fixtures/notification.json"), Notification.class); 68 | assertThat(actual).isEqualTo(notification); 69 | } 70 | 71 | @Test 72 | public void testGetId() throws Exception { 73 | final Notification n1 = Notification.builder().build(); 74 | assertThat(n1.getId("5")).isEqualTo("5"); 75 | final Notification n2 = Notification.create("1000"); 76 | assertThat(n2.getId("6")).isEqualTo("1000"); 77 | } 78 | 79 | @Test 80 | public void testToString() throws Exception { 81 | final ZonedDateTime now = ZonedDateTime.parse("2015-08-14T21:25:19.533Z"); 82 | final Notification n1 = 83 | Notification.builder("test-category", "").withId("1").withCreatedAt(now).build(); 84 | assertThat(n1.toString()) 85 | .isEqualTo( 86 | "Notification{id=Optional[1]," 87 | + " category=test-category, message=," 88 | + " createdAt=2015-08-14T21:25:19.533Z, unseen=Optional.empty," 89 | + " properties={}, notifications=[]}"); 90 | } 91 | 92 | @Test 93 | public void testBuilder() throws Exception { 94 | final Notification n1 = Notification.create("1"); 95 | final Notification n2 = Notification.builder(n1).build(); 96 | assertThat(n1).isEqualTo(n2); 97 | } 98 | 99 | @Test 100 | public void testComparison() throws Exception { 101 | final Notification n1 = Notification.create("1"); 102 | final Notification n2 = Notification.create("2"); 103 | final Notification n3 = Notification.create("3"); 104 | final Notification n4 = Notification.create(""); 105 | 106 | assertThat(n1.compareTo(n2)).isEqualTo(1); 107 | assertThat(n2.compareTo(n1)).isEqualTo(-1); 108 | 109 | final TreeSet notifications = Sets.newTreeSet(); 110 | notifications.add(n1); 111 | notifications.add(n2); 112 | notifications.add(n3); 113 | notifications.add(n4); 114 | 115 | assertThat(notifications).containsExactly(n3, n2, n1, n4); 116 | 117 | final Notification n1b = Notification.builder().withId("1").withUnseen(true).build(); 118 | 119 | assertThat(n1.compareTo(n1b) == 0).isEqualTo(n1.equals(n1b)); 120 | assertThat(n1.equals(null)).isFalse(); 121 | } 122 | 123 | @Test 124 | public void testNaturalOrdering() { 125 | final Notification n1 = Notification.builder("test").withId("1").build(); 126 | final Notification n2 = Notification.builder("test").withId("2").build(); 127 | final Notification n3 = Notification.builder("test").withId("1").build(); 128 | assertThat(n1.equals(n2)).isEqualTo(n1.compareTo(n2) == 0); 129 | assertThat(n2.equals(n3)).isEqualTo(n2.compareTo(n3) == 0); 130 | assertThat(n1.equals(n3)).isEqualTo(n1.compareTo(n3) == 0); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /notification-api/src/test/java/com/smoketurner/notification/api/RuleTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.api; 17 | 18 | import static io.dropwizard.testing.FixtureHelpers.fixture; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | 21 | import com.fasterxml.jackson.databind.JsonMappingException; 22 | import com.fasterxml.jackson.databind.ObjectMapper; 23 | import io.dropwizard.jackson.Jackson; 24 | import io.dropwizard.util.Duration; 25 | import org.junit.Test; 26 | 27 | public class RuleTest { 28 | private final ObjectMapper MAPPER = Jackson.newObjectMapper(); 29 | private final Rule rule = 30 | Rule.builder() 31 | .withMatchOn("first_name") 32 | .withMaxSize(3) 33 | .withMaxDuration(Duration.minutes(10)) 34 | .build(); 35 | 36 | @Test 37 | public void serializesToJSON() throws Exception { 38 | final String actual = MAPPER.writeValueAsString(rule); 39 | final String expected = 40 | MAPPER.writeValueAsString(MAPPER.readValue(fixture("fixtures/rule.json"), Rule.class)); 41 | assertThat(actual).isEqualTo(expected); 42 | } 43 | 44 | @Test 45 | public void deserializesFromJSON() throws Exception { 46 | final Rule actual = MAPPER.readValue(fixture("fixtures/rule.json"), Rule.class); 47 | assertThat(actual).isEqualTo(rule); 48 | } 49 | 50 | @Test(expected = JsonMappingException.class) 51 | public void testInvalidDuration() throws Exception { 52 | MAPPER.readValue(fixture("fixtures/rule_invalid_duration.json"), Rule.class); 53 | } 54 | 55 | @Test(expected = JsonMappingException.class) 56 | public void testInvalidTimeUnit() throws Exception { 57 | MAPPER.readValue(fixture("fixtures/rule_invalid_timeunit.json"), Rule.class); 58 | } 59 | 60 | @Test 61 | public void testInvalidMatchOn() throws Exception { 62 | final Rule actual = MAPPER.readValue(fixture("fixtures/rule_invalid_matchon.json"), Rule.class); 63 | assertThat(actual.getMatchOn().isPresent()).isFalse(); 64 | } 65 | 66 | @Test 67 | public void testIsValidMatchOn() throws Exception { 68 | Rule rule = Rule.builder().build(); 69 | assertThat(rule.isValid()).isFalse(); 70 | 71 | rule = Rule.builder().withMatchOn(null).build(); 72 | assertThat(rule.isValid()).isFalse(); 73 | 74 | rule = Rule.builder().withMatchOn("").build(); 75 | assertThat(rule.isValid()).isFalse(); 76 | 77 | rule = Rule.builder().withMatchOn("like").build(); 78 | assertThat(rule.isValid()).isTrue(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /notification-api/src/test/resources/fixtures/notification.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "12345", 3 | "category": "new-follower", 4 | "message": "you have a new follower", 5 | "created_at": "2015-06-29T21:04:12Z", 6 | "unseen": true, 7 | "properties": { 8 | "first_name": "Test", 9 | "last_name": "User" 10 | }, 11 | "notifications": [ 12 | { 13 | "id": "12346", 14 | "category": "new-follower", 15 | "message": "you have a new follower", 16 | "created_at": "2015-06-29T21:04:12Z", 17 | "unseen": true, 18 | "properties": { 19 | "first_name": "Test 2", 20 | "last_name": "User 2" 21 | } 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /notification-api/src/test/resources/fixtures/rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "match_on": "first_name", 3 | "max_size": 3, 4 | "max_duration": "10 minutes" 5 | } -------------------------------------------------------------------------------- /notification-api/src/test/resources/fixtures/rule_invalid_duration.json: -------------------------------------------------------------------------------- 1 | { 2 | "match_on": "first_name", 3 | "max_size": 3, 4 | "max_duration": "test" 5 | } -------------------------------------------------------------------------------- /notification-api/src/test/resources/fixtures/rule_invalid_matchon.json: -------------------------------------------------------------------------------- 1 | { 2 | "match_on": "", 3 | "max_size": 3, 4 | "max_duration": "3 days" 5 | } -------------------------------------------------------------------------------- /notification-api/src/test/resources/fixtures/rule_invalid_timeunit.json: -------------------------------------------------------------------------------- 1 | { 2 | "match_on": "first_name", 3 | "max_size": 3, 4 | "max_duration": "3 lightyears" 5 | } -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/config/NotificationConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.config; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | import com.smoketurner.dropwizard.graphql.GraphQLFactory; 20 | import com.smoketurner.dropwizard.riak.RiakFactory; 21 | import io.dropwizard.Configuration; 22 | import io.dropwizard.util.Duration; 23 | import io.dropwizard.validation.MinDuration; 24 | import java.util.concurrent.TimeUnit; 25 | import javax.validation.Valid; 26 | import javax.validation.constraints.NotNull; 27 | 28 | public class NotificationConfiguration extends Configuration { 29 | 30 | @NotNull 31 | @MinDuration(value = 1, unit = TimeUnit.SECONDS) 32 | private Duration ruleCacheTimeout = Duration.minutes(5); 33 | 34 | @NotNull 35 | @MinDuration(value = 1, unit = TimeUnit.MILLISECONDS) 36 | private Duration riakTimeout = Duration.seconds(60); 37 | 38 | @NotNull 39 | @MinDuration(value = 1, unit = TimeUnit.MILLISECONDS) 40 | private Duration riakRequestTimeout = Duration.seconds(5); 41 | 42 | @Valid @NotNull @JsonProperty private final RiakFactory riak = new RiakFactory(); 43 | 44 | @Valid @NotNull @JsonProperty private final GraphQLFactory graphql = new GraphQLFactory(); 45 | 46 | @JsonProperty 47 | public Duration getRiakTimeout() { 48 | return riakTimeout; 49 | } 50 | 51 | @JsonProperty 52 | public void setRiakTimeout(final Duration timeout) { 53 | this.riakTimeout = timeout; 54 | } 55 | 56 | @JsonProperty 57 | public Duration getRiakRequestTimeout() { 58 | return riakRequestTimeout; 59 | } 60 | 61 | @JsonProperty 62 | public void setRiakRequestTimeout(final Duration timeout) { 63 | this.riakRequestTimeout = timeout; 64 | } 65 | 66 | @JsonProperty 67 | public Duration getRuleCacheTimeout() { 68 | return ruleCacheTimeout; 69 | } 70 | 71 | @JsonProperty 72 | public void setRuleCacheTimeout(final Duration timeout) { 73 | this.ruleCacheTimeout = timeout; 74 | } 75 | 76 | @JsonProperty 77 | public RiakFactory getRiak() { 78 | return riak; 79 | } 80 | 81 | @JsonProperty 82 | public GraphQLFactory getGraphQL() { 83 | return graphql; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/core/IdGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.core; 17 | 18 | import com.amirkhawaja.Ksuid; 19 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 20 | import java.io.IOException; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | public class IdGenerator { 25 | 26 | private static final Logger LOGGER = LoggerFactory.getLogger(IdGenerator.class); 27 | private final Ksuid ksuid; 28 | 29 | /** Constructor */ 30 | public IdGenerator() { 31 | this.ksuid = new Ksuid(); 32 | } 33 | 34 | /** 35 | * Generate a new notification ID 36 | * 37 | * @return the new notification ID 38 | * @throws NotificationStoreException if unable to generate an ID 39 | */ 40 | public String nextId() throws NotificationStoreException { 41 | try { 42 | return ksuid.generate(); 43 | } catch (IOException e) { 44 | LOGGER.error("Unable to generate new ID", e); 45 | throw new NotificationStoreException(e); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/core/Rollup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.core; 17 | 18 | import com.smoketurner.notification.api.Notification; 19 | import com.smoketurner.notification.api.Rule; 20 | import java.util.Map; 21 | import java.util.Objects; 22 | import java.util.TreeSet; 23 | import java.util.stream.Stream; 24 | 25 | public class Rollup { 26 | 27 | private final Map rules; 28 | private final TreeSet matchers = new TreeSet<>(); 29 | 30 | /** 31 | * Constructor 32 | * 33 | * @param rules Map of rules 34 | */ 35 | public Rollup(final Map rules) { 36 | this.rules = Objects.requireNonNull(rules, "rules == null"); 37 | } 38 | 39 | /** 40 | * Iterates over the notifications and uses the {@link Rule} and {@link Matcher} objects to roll 41 | * up the notifications based on the rules. 42 | * 43 | * @param notifications Notifications to roll up 44 | * @return Rolled up notifications 45 | */ 46 | public Stream rollup(final Stream notifications) { 47 | Objects.requireNonNull(notifications, "notifications == null"); 48 | 49 | if (rules.isEmpty()) { 50 | return notifications; 51 | } 52 | 53 | final TreeSet rollups = new TreeSet<>(); 54 | 55 | notifications.forEachOrdered( 56 | notification -> { 57 | final Rule rule = rules.get(notification.getCategory()); 58 | 59 | // If the notification category doesn't match any rule categories, 60 | // add the notification as-is to the list of rollups. 61 | if (rule == null || !rule.isValid()) { 62 | rollups.add(notification); 63 | } else if (matchers.isEmpty()) { 64 | // If we don't have any matchers yet, add the first one 65 | matchers.add(new Matcher(rule, notification)); 66 | } else { 67 | // Loop through the existing matchers to see if this 68 | // notification falls into any previous rollups 69 | boolean matched = false; 70 | for (final Matcher matcher : matchers) { 71 | if (matcher.test(notification)) { 72 | matched = true; 73 | 74 | // if the matcher is now full, add it to the rollups and 75 | // remove it from the available matchers which still 76 | // have empty space. 77 | if (matcher.isFull()) { 78 | matchers.remove(matcher); 79 | rollups.add(matcher.getNotification()); 80 | } 81 | break; 82 | } 83 | } 84 | 85 | // If the notification didn't match any existing rollups, add it 86 | // as a new matcher 87 | if (!matched) { 88 | matchers.add(new Matcher(rule, notification)); 89 | } 90 | } 91 | }); 92 | 93 | // Pull out the rolled up notifications out of the matchers 94 | for (final Matcher match : matchers) { 95 | rollups.add(match.getNotification()); 96 | } 97 | 98 | return rollups.stream(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/core/StringSetParam.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.core; 17 | 18 | import com.google.common.base.Splitter; 19 | import com.google.common.base.Strings; 20 | import com.google.common.collect.ImmutableSet; 21 | import io.dropwizard.jersey.params.AbstractParam; 22 | import java.util.Collections; 23 | import java.util.Set; 24 | import javax.annotation.Nullable; 25 | 26 | public class StringSetParam extends AbstractParam> { 27 | 28 | public StringSetParam(String input) { 29 | super(input); 30 | } 31 | 32 | @Override 33 | protected Set parse(@Nullable final String input) throws Exception { 34 | if (Strings.isNullOrEmpty(input)) { 35 | return Collections.emptySet(); 36 | } 37 | 38 | final Iterable splitter = 39 | Splitter.on(',').omitEmptyStrings().trimResults().split(input); 40 | 41 | return ImmutableSet.copyOf(splitter); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/core/UserNotifications.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.core; 17 | 18 | import com.google.common.collect.ImmutableSortedSet; 19 | import com.google.common.collect.Iterables; 20 | import com.smoketurner.notification.api.Notification; 21 | import java.util.Collections; 22 | import java.util.Objects; 23 | import java.util.StringJoiner; 24 | import java.util.TreeSet; 25 | import java.util.stream.Collectors; 26 | import java.util.stream.Stream; 27 | 28 | public final class UserNotifications { 29 | 30 | private final Iterable unseen; 31 | private final Iterable seen; 32 | 33 | /** 34 | * Constructor 35 | * 36 | * @param unseen Unseen notifications 37 | * @param seen Seen notifications 38 | */ 39 | public UserNotifications(final Iterable unseen, final Iterable seen) { 40 | this.unseen = Objects.requireNonNull(unseen, "unseen == null"); 41 | this.seen = Objects.requireNonNull(seen, "seen == null"); 42 | } 43 | 44 | /** 45 | * Constructor 46 | * 47 | * @param unseen Unseen notifications 48 | * @param seen Seen notifications 49 | */ 50 | public UserNotifications(final Stream unseen, final Stream seen) { 51 | Objects.requireNonNull(unseen, "unseen == null"); 52 | Objects.requireNonNull(seen, "seen == null"); 53 | 54 | this.unseen = unseen.collect(Collectors.toCollection(TreeSet::new)); 55 | this.seen = seen.collect(Collectors.toCollection(TreeSet::new)); 56 | } 57 | 58 | /** 59 | * Constructor 60 | * 61 | * @param unseen Unseen notifications 62 | */ 63 | public UserNotifications(final Iterable unseen) { 64 | this.unseen = Objects.requireNonNull(unseen, "unseen == null"); 65 | this.seen = Collections.emptySortedSet(); 66 | } 67 | 68 | /** 69 | * Constructor 70 | * 71 | * @param unseen Unseen notifications 72 | */ 73 | public UserNotifications(final Stream unseen) { 74 | Objects.requireNonNull(unseen, "unseen == null"); 75 | this.unseen = unseen.collect(Collectors.toCollection(TreeSet::new)); 76 | this.seen = Collections.emptySortedSet(); 77 | } 78 | 79 | /** Constructor */ 80 | public UserNotifications() { 81 | this.unseen = Collections.emptySortedSet(); 82 | this.seen = Collections.emptySortedSet(); 83 | } 84 | 85 | public boolean isEmpty() { 86 | return Iterables.isEmpty(unseen) && Iterables.isEmpty(seen); 87 | } 88 | 89 | public Iterable getUnseen() { 90 | return unseen; 91 | } 92 | 93 | public Iterable getSeen() { 94 | return seen; 95 | } 96 | 97 | public ImmutableSortedSet getNotifications() { 98 | return ImmutableSortedSet.naturalOrder().addAll(unseen).addAll(seen).build(); 99 | } 100 | 101 | @Override 102 | public boolean equals(final Object obj) { 103 | if (this == obj) { 104 | return true; 105 | } 106 | if ((obj == null) || (getClass() != obj.getClass())) { 107 | return false; 108 | } 109 | 110 | final UserNotifications other = (UserNotifications) obj; 111 | return Objects.equals(unseen, other.unseen) && Objects.equals(seen, other.seen); 112 | } 113 | 114 | @Override 115 | public int hashCode() { 116 | return Objects.hash(unseen, seen); 117 | } 118 | 119 | @Override 120 | public String toString() { 121 | return new StringJoiner(", ", UserNotifications.class.getSimpleName() + "{", "}") 122 | .add("unseen=" + unseen) 123 | .add("seen=" + seen) 124 | .toString(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/core/WebSecurityFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.core; 17 | 18 | import com.google.common.net.HttpHeaders; 19 | import java.io.IOException; 20 | import javax.annotation.Priority; 21 | import javax.ws.rs.Priorities; 22 | import javax.ws.rs.container.ContainerRequestContext; 23 | import javax.ws.rs.container.ContainerResponseContext; 24 | import javax.ws.rs.container.ContainerResponseFilter; 25 | import javax.ws.rs.core.MultivaluedMap; 26 | import javax.ws.rs.ext.Provider; 27 | 28 | @Provider 29 | @Priority(Priorities.USER) 30 | public class WebSecurityFilter implements ContainerResponseFilter { 31 | 32 | @Override 33 | public void filter(ContainerRequestContext request, ContainerResponseContext response) 34 | throws IOException { 35 | 36 | final MultivaluedMap headers = response.getHeaders(); 37 | 38 | headers.putSingle(HttpHeaders.X_CONTENT_TYPE_OPTIONS, "nosniff"); 39 | headers.putSingle(HttpHeaders.X_FRAME_OPTIONS, "deny"); 40 | headers.putSingle(HttpHeaders.X_XSS_PROTECTION, "1; mode=block"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/exceptions/NotificationException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.exceptions; 17 | 18 | import javax.ws.rs.WebApplicationException; 19 | import javax.ws.rs.core.Response; 20 | 21 | public class NotificationException extends WebApplicationException { 22 | 23 | private static final long serialVersionUID = 4359514441284675684L; 24 | private final int code; 25 | private final Response.Status status; 26 | private final String message; 27 | 28 | /** 29 | * Constructor 30 | * 31 | * @param code Status code to return 32 | * @param message Error message to return 33 | */ 34 | public NotificationException(final int code, final String message) { 35 | super(code); 36 | this.code = code; 37 | this.status = Response.Status.fromStatusCode(code); 38 | this.message = message; 39 | } 40 | 41 | /** 42 | * Constructor 43 | * 44 | * @param status Status code to return 45 | * @param message Error message to return 46 | */ 47 | public NotificationException(final Response.Status status, final String message) { 48 | super(status); 49 | this.code = status.getStatusCode(); 50 | this.status = status; 51 | this.message = message; 52 | } 53 | 54 | /** 55 | * Constructor 56 | * 57 | * @param status Status code to return 58 | * @param message Error message to return 59 | * @param cause Throwable which caused the exception 60 | */ 61 | public NotificationException( 62 | final Response.Status status, final String message, final Throwable cause) { 63 | super(cause, status); 64 | this.code = status.getStatusCode(); 65 | this.status = status; 66 | this.message = message; 67 | } 68 | 69 | public int getCode() { 70 | return code; 71 | } 72 | 73 | public Response.Status getStatus() { 74 | return status; 75 | } 76 | 77 | @Override 78 | public String getMessage() { 79 | return message; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/exceptions/NotificationExceptionMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.exceptions; 17 | 18 | import io.dropwizard.jersey.errors.ErrorMessage; 19 | import javax.ws.rs.core.MediaType; 20 | import javax.ws.rs.core.Response; 21 | import javax.ws.rs.ext.ExceptionMapper; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | public class NotificationExceptionMapper implements ExceptionMapper { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(NotificationExceptionMapper.class); 28 | 29 | @Override 30 | public Response toResponse(final NotificationException exception) { 31 | LOGGER.debug("Error response ({}): {}", exception.getCode(), exception.getMessage()); 32 | 33 | return Response.status(exception.getCode()) 34 | .entity(new ErrorMessage(exception.getCode(), exception.getMessage())) 35 | .type(MediaType.APPLICATION_JSON) 36 | .build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/exceptions/NotificationStoreException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.exceptions; 17 | 18 | public class NotificationStoreException extends Exception { 19 | 20 | private static final long serialVersionUID = 1L; 21 | 22 | public NotificationStoreException() { 23 | super(); 24 | } 25 | 26 | public NotificationStoreException(final Throwable cause) { 27 | super(cause); 28 | } 29 | 30 | public NotificationStoreException(final String message) { 31 | super(message); 32 | } 33 | 34 | public NotificationStoreException(final String message, final Throwable cause) { 35 | super(message, cause); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/graphql/CreateNotificationMutation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import com.google.common.base.Strings; 19 | import com.smoketurner.dropwizard.graphql.GraphQLValidationError; 20 | import com.smoketurner.notification.api.Notification; 21 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 22 | import com.smoketurner.notification.application.store.NotificationStore; 23 | import graphql.schema.DataFetcher; 24 | import graphql.schema.DataFetchingEnvironment; 25 | import java.util.Collections; 26 | import java.util.Map; 27 | import java.util.Objects; 28 | import java.util.stream.Collectors; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | public class CreateNotificationMutation implements DataFetcher { 33 | 34 | private static final Logger LOGGER = LoggerFactory.getLogger(CreateNotificationMutation.class); 35 | private final NotificationStore store; 36 | 37 | /** 38 | * Constructor 39 | * 40 | * @param store Notification data store 41 | */ 42 | public CreateNotificationMutation(final NotificationStore store) { 43 | this.store = Objects.requireNonNull(store, "store == null"); 44 | } 45 | 46 | @Override 47 | public Notification get(DataFetchingEnvironment environment) { 48 | final String username = environment.getArgument("username"); 49 | if (Strings.isNullOrEmpty(username)) { 50 | throw new GraphQLValidationError("username cannot be empty"); 51 | } 52 | 53 | final Map input = environment.getArgument("notification"); 54 | if (input == null || input.isEmpty()) { 55 | throw new GraphQLValidationError("notification cannot be empty"); 56 | } 57 | 58 | final String category = String.valueOf(input.get("category")).trim(); 59 | if (Strings.isNullOrEmpty(category) || "null".equals(category)) { 60 | throw new GraphQLValidationError("category cannot be empty"); 61 | } 62 | 63 | final int categoryLength = category.codePointCount(0, category.length()); 64 | 65 | if (categoryLength < Notification.CATEGORY_MIN_LENGTH 66 | || categoryLength > Notification.CATEGORY_MAX_LENGTH) { 67 | throw new GraphQLValidationError( 68 | String.format( 69 | "category must be between %d and %d characters", 70 | Notification.CATEGORY_MIN_LENGTH, Notification.CATEGORY_MAX_LENGTH)); 71 | } 72 | 73 | final String message = String.valueOf(input.get("message")).trim(); 74 | if (Strings.isNullOrEmpty(message) || "null".equals(message)) { 75 | throw new GraphQLValidationError("message cannot be empty"); 76 | } 77 | 78 | final int messageLength = message.codePointCount(0, message.length()); 79 | 80 | if (messageLength < Notification.MESSAGE_MIN_LENGTH 81 | || messageLength > Notification.MESSAGE_MAX_LENGTH) { 82 | throw new GraphQLValidationError( 83 | String.format( 84 | "message must be between %d and %d characters", 85 | Notification.MESSAGE_MIN_LENGTH, Notification.MESSAGE_MAX_LENGTH)); 86 | } 87 | 88 | final Notification.Builder builder = Notification.builder(category, message); 89 | 90 | final Object properties = input.get("properties"); 91 | if (properties != null && properties instanceof Map) { 92 | builder.withProperties(convertToMap(properties)); 93 | } 94 | 95 | final Notification notification = builder.build(); 96 | 97 | try { 98 | return store.store(username, notification); 99 | } catch (NotificationStoreException e) { 100 | LOGGER.error(String.format("Unable to create notification for %s", username), e); 101 | throw new GraphQLValidationError("Unable to create notification"); 102 | } 103 | } 104 | 105 | /** 106 | * Safely convert an Object into a Map of string objects. 107 | * 108 | * @param obj Object to convert 109 | * @return Map of properties 110 | */ 111 | private static Map convertToMap(final Object obj) { 112 | if (obj == null) { 113 | return Collections.emptyMap(); 114 | } 115 | 116 | @SuppressWarnings("unchecked") 117 | final Map map = (Map) obj; 118 | if (map.isEmpty()) { 119 | return Collections.emptyMap(); 120 | } 121 | 122 | return map.entrySet().stream() 123 | .collect(Collectors.toMap(Map.Entry::getKey, e -> String.valueOf(e.getValue()).trim())); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/graphql/CreateRuleMutation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import com.google.common.base.Strings; 19 | import com.smoketurner.dropwizard.graphql.GraphQLValidationError; 20 | import com.smoketurner.notification.api.Notification; 21 | import com.smoketurner.notification.api.Rule; 22 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 23 | import com.smoketurner.notification.application.store.RuleStore; 24 | import graphql.schema.DataFetcher; 25 | import graphql.schema.DataFetchingEnvironment; 26 | import io.dropwizard.util.Duration; 27 | import java.util.Map; 28 | import java.util.Objects; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | public class CreateRuleMutation implements DataFetcher { 33 | 34 | private static final Logger LOGGER = LoggerFactory.getLogger(CreateRuleMutation.class); 35 | private final RuleStore store; 36 | 37 | /** 38 | * Constructor 39 | * 40 | * @param store Rule data store 41 | */ 42 | public CreateRuleMutation(final RuleStore store) { 43 | this.store = Objects.requireNonNull(store, "store == null"); 44 | } 45 | 46 | @Override 47 | public Boolean get(DataFetchingEnvironment environment) { 48 | final String category = environment.getArgument("category"); 49 | if (Strings.isNullOrEmpty(category)) { 50 | throw new GraphQLValidationError("category cannot be empty"); 51 | } 52 | 53 | final int categoryLength = category.codePointCount(0, category.length()); 54 | 55 | if (categoryLength < Notification.CATEGORY_MIN_LENGTH 56 | || categoryLength > Notification.CATEGORY_MAX_LENGTH) { 57 | throw new GraphQLValidationError( 58 | String.format( 59 | "category must be between %d and %d characters", 60 | Notification.CATEGORY_MIN_LENGTH, Notification.CATEGORY_MAX_LENGTH)); 61 | } 62 | 63 | final Map input = environment.getArgument("rule"); 64 | if (input == null || input.isEmpty()) { 65 | throw new GraphQLValidationError("rule cannot be empty"); 66 | } 67 | 68 | final Rule.Builder builder = Rule.builder(); 69 | if (input.containsKey("maxSize")) { 70 | try { 71 | builder.withMaxSize(Integer.parseInt(String.valueOf(input.get("maxSize")))); 72 | } catch (NumberFormatException e) { 73 | throw new GraphQLValidationError("maxSize is not an integer"); 74 | } 75 | } 76 | if (input.containsKey("maxDuration")) { 77 | try { 78 | builder.withMaxDuration(Duration.parse(String.valueOf(input.get("maxDuration")))); 79 | } catch (IllegalArgumentException e) { 80 | throw new GraphQLValidationError("maxDuration is an invalid duration"); 81 | } 82 | } 83 | if (input.containsKey("matchOn")) { 84 | builder.withMatchOn(String.valueOf(input.get("matchOn"))); 85 | } 86 | 87 | final Rule rule = builder.build(); 88 | 89 | if (!rule.isValid()) { 90 | throw new GraphQLValidationError("rule cannot be empty"); 91 | } 92 | 93 | try { 94 | store.store(category, rule); 95 | } catch (NotificationStoreException e) { 96 | LOGGER.error(String.format("Unable to create rule for %s", category), e); 97 | throw new GraphQLValidationError("Unable to create rule"); 98 | } 99 | 100 | return true; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/graphql/NotificationDataFetcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import com.google.common.base.Strings; 19 | import com.smoketurner.notification.api.Notification; 20 | import com.smoketurner.notification.application.core.UserNotifications; 21 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 22 | import com.smoketurner.notification.application.store.NotificationStore; 23 | import graphql.schema.DataFetcher; 24 | import graphql.schema.DataFetchingEnvironment; 25 | import java.util.Collections; 26 | import java.util.Objects; 27 | import java.util.Optional; 28 | import java.util.SortedSet; 29 | import javax.annotation.Nullable; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | public class NotificationDataFetcher implements DataFetcher> { 34 | 35 | private static final Logger LOGGER = LoggerFactory.getLogger(NotificationDataFetcher.class); 36 | private final NotificationStore store; 37 | 38 | /** 39 | * Constructor 40 | * 41 | * @param store Notification data store 42 | */ 43 | public NotificationDataFetcher(final NotificationStore store) { 44 | this.store = Objects.requireNonNull(store, "store == null"); 45 | } 46 | 47 | @Nullable 48 | @Override 49 | public SortedSet get(DataFetchingEnvironment environment) { 50 | final String username = environment.getArgument("username"); 51 | if (Strings.isNullOrEmpty(username)) { 52 | return null; 53 | } 54 | 55 | final Optional notifications; 56 | try { 57 | notifications = store.fetch(username); 58 | } catch (NotificationStoreException e) { 59 | LOGGER.error("Unable to fetch notifications", e); 60 | return null; 61 | } 62 | 63 | if (!notifications.isPresent()) { 64 | return Collections.emptySortedSet(); 65 | } 66 | 67 | return notifications.get().getNotifications(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/graphql/RemoveAllNotificationsMutation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import com.google.common.base.Strings; 19 | import com.smoketurner.dropwizard.graphql.GraphQLValidationError; 20 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 21 | import com.smoketurner.notification.application.store.NotificationStore; 22 | import graphql.schema.DataFetcher; 23 | import graphql.schema.DataFetchingEnvironment; 24 | import java.util.Objects; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | public class RemoveAllNotificationsMutation implements DataFetcher { 29 | 30 | private static final Logger LOGGER = 31 | LoggerFactory.getLogger(RemoveAllNotificationsMutation.class); 32 | private final NotificationStore store; 33 | 34 | /** 35 | * Constructor 36 | * 37 | * @param store Notification data store 38 | */ 39 | public RemoveAllNotificationsMutation(final NotificationStore store) { 40 | this.store = Objects.requireNonNull(store, "store == null"); 41 | } 42 | 43 | @Override 44 | public Boolean get(DataFetchingEnvironment environment) { 45 | final String username = environment.getArgument("username"); 46 | if (Strings.isNullOrEmpty(username)) { 47 | throw new GraphQLValidationError("username cannot be empty"); 48 | } 49 | 50 | try { 51 | store.removeAll(username); 52 | } catch (NotificationStoreException e) { 53 | LOGGER.error(String.format("Unable to remove all notifications for %s", username), e); 54 | throw new GraphQLValidationError("Unable to remove all notifications"); 55 | } 56 | 57 | return true; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/graphql/RemoveAllRulesMutation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import com.smoketurner.dropwizard.graphql.GraphQLValidationError; 19 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 20 | import com.smoketurner.notification.application.store.RuleStore; 21 | import graphql.schema.DataFetcher; 22 | import graphql.schema.DataFetchingEnvironment; 23 | import java.util.Objects; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | public class RemoveAllRulesMutation implements DataFetcher { 28 | 29 | private static final Logger LOGGER = LoggerFactory.getLogger(RemoveAllRulesMutation.class); 30 | private final RuleStore store; 31 | 32 | /** 33 | * Constructor 34 | * 35 | * @param store Rule data store 36 | */ 37 | public RemoveAllRulesMutation(final RuleStore store) { 38 | this.store = Objects.requireNonNull(store, "store == null"); 39 | } 40 | 41 | @Override 42 | public Boolean get(DataFetchingEnvironment environment) { 43 | try { 44 | store.removeAll(); 45 | } catch (NotificationStoreException e) { 46 | LOGGER.error("Unable to remove all rules", e); 47 | throw new GraphQLValidationError("Unable to remove all rules"); 48 | } 49 | 50 | return true; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/graphql/RemoveNotificationMutation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import com.google.common.base.Strings; 19 | import com.smoketurner.dropwizard.graphql.GraphQLValidationError; 20 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 21 | import com.smoketurner.notification.application.store.NotificationStore; 22 | import graphql.schema.DataFetcher; 23 | import graphql.schema.DataFetchingEnvironment; 24 | import java.util.List; 25 | import java.util.Objects; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | public class RemoveNotificationMutation implements DataFetcher { 30 | 31 | private static final Logger LOGGER = LoggerFactory.getLogger(RemoveNotificationMutation.class); 32 | private final NotificationStore store; 33 | 34 | /** 35 | * Constructor 36 | * 37 | * @param store Notification data store 38 | */ 39 | public RemoveNotificationMutation(final NotificationStore store) { 40 | this.store = Objects.requireNonNull(store, "store == null"); 41 | } 42 | 43 | @Override 44 | public Boolean get(DataFetchingEnvironment environment) { 45 | final String username = environment.getArgument("username"); 46 | if (Strings.isNullOrEmpty(username)) { 47 | throw new GraphQLValidationError("username cannot be empty"); 48 | } 49 | 50 | final List ids = environment.getArgument("ids"); 51 | if (ids == null || ids.isEmpty()) { 52 | return false; 53 | } 54 | 55 | try { 56 | store.remove(username, ids); 57 | } catch (NotificationStoreException e) { 58 | LOGGER.error(String.format("Unable to remove notifications for %s", username), e); 59 | throw new GraphQLValidationError("Unable to remove notifications"); 60 | } 61 | 62 | return true; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/graphql/RemoveRuleMutation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import com.google.common.base.Strings; 19 | import com.smoketurner.dropwizard.graphql.GraphQLValidationError; 20 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 21 | import com.smoketurner.notification.application.store.RuleStore; 22 | import graphql.schema.DataFetcher; 23 | import graphql.schema.DataFetchingEnvironment; 24 | import java.util.Objects; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | public class RemoveRuleMutation implements DataFetcher { 29 | 30 | private static final Logger LOGGER = LoggerFactory.getLogger(RemoveRuleMutation.class); 31 | private final RuleStore store; 32 | 33 | /** 34 | * Constructor 35 | * 36 | * @param store Rule data store 37 | */ 38 | public RemoveRuleMutation(final RuleStore store) { 39 | this.store = Objects.requireNonNull(store, "store == null"); 40 | } 41 | 42 | @Override 43 | public Boolean get(DataFetchingEnvironment environment) { 44 | final String category = environment.getArgument("category"); 45 | if (Strings.isNullOrEmpty(category)) { 46 | throw new GraphQLValidationError("category cannot be empty"); 47 | } 48 | 49 | try { 50 | store.remove(category); 51 | } catch (NotificationStoreException e) { 52 | LOGGER.error(String.format("Unable to remove rule for %s", category), e); 53 | throw new GraphQLValidationError("Unable to remove rule"); 54 | } 55 | 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/graphql/RuleDataFetcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import com.google.common.collect.ImmutableMap; 19 | import com.smoketurner.notification.api.Rule; 20 | import com.smoketurner.notification.application.store.RuleStore; 21 | import graphql.schema.DataFetcher; 22 | import graphql.schema.DataFetchingEnvironment; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.Objects; 26 | import java.util.stream.Collectors; 27 | 28 | public class RuleDataFetcher implements DataFetcher>> { 29 | 30 | private final RuleStore store; 31 | 32 | /** 33 | * Constructor 34 | * 35 | * @param store Rule data store 36 | */ 37 | public RuleDataFetcher(final RuleStore store) { 38 | this.store = Objects.requireNonNull(store, "store == null"); 39 | } 40 | 41 | @Override 42 | public List> get(DataFetchingEnvironment environment) { 43 | final Map data = store.fetchCached(); 44 | 45 | return data.entrySet().stream() 46 | .map(e -> ImmutableMap.of("category", e.getKey(), "rule", e.getValue())) 47 | .collect(Collectors.toList()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/graphql/Scalars.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import graphql.language.ArrayValue; 19 | import graphql.language.BooleanValue; 20 | import graphql.language.EnumValue; 21 | import graphql.language.FloatValue; 22 | import graphql.language.IntValue; 23 | import graphql.language.NullValue; 24 | import graphql.language.ObjectValue; 25 | import graphql.language.StringValue; 26 | import graphql.language.Value; 27 | import graphql.schema.Coercing; 28 | import graphql.schema.CoercingParseLiteralException; 29 | import graphql.schema.CoercingParseValueException; 30 | import graphql.schema.GraphQLScalarType; 31 | import java.util.Arrays; 32 | import java.util.LinkedHashMap; 33 | import java.util.Map; 34 | import java.util.stream.Collectors; 35 | import javax.annotation.Nullable; 36 | 37 | /** 38 | * Portions copied from 39 | * https://github.com/leangen/graphql-spqr/blob/master/src/main/java/io/leangen/graphql/util/Scalars.java 40 | */ 41 | public class Scalars { 42 | 43 | private static Coercing MAP_SCALAR_COERCION = 44 | new Coercing() { 45 | @Override 46 | public Object serialize(Object dataFetcherResult) { 47 | return dataFetcherResult; 48 | } 49 | 50 | @Override 51 | public Object parseValue(Object input) { 52 | if (input instanceof Map) { 53 | return input; 54 | } 55 | throw valueParsingException(input, Map.class); 56 | } 57 | 58 | @Nullable 59 | @Override 60 | public Object parseLiteral(Object input) { 61 | return parseObjectValue(literalOrException(input, ObjectValue.class)); 62 | } 63 | }; 64 | 65 | public static GraphQLScalarType graphQLMapScalar(String name) { 66 | return new GraphQLScalarType( 67 | name, "Built-in scalar for map-like structures", MAP_SCALAR_COERCION); 68 | } 69 | 70 | @Nullable 71 | private static Object parseObjectValue(Value value) { 72 | if (value instanceof StringValue) { 73 | return ((StringValue) value).getValue(); 74 | } 75 | if (value instanceof IntValue) { 76 | return ((IntValue) value).getValue(); 77 | } 78 | if (value instanceof FloatValue) { 79 | return ((FloatValue) value).getValue(); 80 | } 81 | if (value instanceof BooleanValue) { 82 | return ((BooleanValue) value).isValue(); 83 | } 84 | if (value instanceof EnumValue) { 85 | return ((EnumValue) value).getName(); 86 | } 87 | if (value instanceof NullValue) { 88 | return null; 89 | } 90 | if (value instanceof ArrayValue) { 91 | return ((ArrayValue) value) 92 | .getValues().stream().map(Scalars::parseObjectValue).collect(Collectors.toList()); 93 | } 94 | if (value instanceof ObjectValue) { 95 | final Map map = new LinkedHashMap<>(); 96 | ((ObjectValue) value) 97 | .getObjectFields() 98 | .forEach(field -> map.put(field.getName(), parseObjectValue(field.getValue()))); 99 | return map; 100 | } 101 | // Should never happen, as it would mean the variable was not replaced by the parser 102 | throw new CoercingParseLiteralException( 103 | "Unknown scalar AST type: " + value.getClass().getName()); 104 | } 105 | 106 | public static > T literalOrException(Object input, Class valueType) { 107 | if (valueType.isInstance(input)) { 108 | return valueType.cast(input); 109 | } 110 | throw new CoercingParseLiteralException(errorMessage(input, valueType)); 111 | } 112 | 113 | public static CoercingParseLiteralException literalParsingException( 114 | Object input, Class... allowedTypes) { 115 | return new CoercingParseLiteralException(errorMessage(input, allowedTypes)); 116 | } 117 | 118 | public static CoercingParseValueException valueParsingException( 119 | Object input, Class... allowedTypes) { 120 | return new CoercingParseValueException(errorMessage(input, allowedTypes)); 121 | } 122 | 123 | public static String errorMessage(Object input, Class... allowedTypes) { 124 | String types = 125 | Arrays.stream(allowedTypes) 126 | .map(type -> "'" + type.getSimpleName() + "'") 127 | .collect(Collectors.joining(" or ")); 128 | return String.format( 129 | "Expected %stype %s but was '%s'", 130 | input instanceof Value ? "AST " : "", 131 | types, 132 | input == null ? "null" : input.getClass().getSimpleName()); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/graphql/UsernameFieldValidation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import com.google.common.base.Strings; 19 | import com.google.common.collect.ImmutableList; 20 | import graphql.GraphQLError; 21 | import graphql.execution.instrumentation.fieldvalidation.FieldAndArguments; 22 | import graphql.execution.instrumentation.fieldvalidation.FieldValidation; 23 | import graphql.execution.instrumentation.fieldvalidation.FieldValidationEnvironment; 24 | import graphql.language.Field; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | public class UsernameFieldValidation implements FieldValidation { 31 | 32 | private static final Logger LOGGER = LoggerFactory.getLogger(UsernameFieldValidation.class); 33 | private static final List VALID_FIELDS = 34 | ImmutableList.of( 35 | "notifications", "createNotification", "removeNotification", "removeAllNotifications"); 36 | 37 | private static final int USERNAME_MIN_LENGTH = 3; 38 | private static final int USERNAME_MAX_LENGTH = 64; 39 | 40 | @Override 41 | public List validateFields(FieldValidationEnvironment environment) { 42 | final List errors = new ArrayList<>(); 43 | 44 | for (FieldAndArguments fieldAndArguments : environment.getFields()) { 45 | final Field field = fieldAndArguments.getField(); 46 | if (!VALID_FIELDS.contains(field.getName())) { 47 | continue; 48 | } 49 | 50 | LOGGER.debug("Field: {}", field.getName()); 51 | 52 | final String username = fieldAndArguments.getArgumentValue("username"); 53 | 54 | if (Strings.isNullOrEmpty(username)) { 55 | errors.add(environment.mkError("username cannot be empty", fieldAndArguments)); 56 | } else { 57 | final int length = username.codePointCount(0, username.length()); 58 | 59 | if (length < USERNAME_MIN_LENGTH || length > USERNAME_MAX_LENGTH) { 60 | errors.add( 61 | environment.mkError( 62 | String.format( 63 | "username must be between %d and %d characters", 64 | USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH), 65 | fieldAndArguments)); 66 | } else if (!username.matches("[A-Za-z0-9]+")) { 67 | errors.add( 68 | environment.mkError( 69 | "username must only contain alphanumeric characters", fieldAndArguments)); 70 | } 71 | } 72 | } 73 | 74 | return errors; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/managed/CursorStoreManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.managed; 17 | 18 | import com.smoketurner.notification.application.store.CursorStore; 19 | import io.dropwizard.lifecycle.Managed; 20 | import java.util.Objects; 21 | 22 | public class CursorStoreManager implements Managed { 23 | 24 | private final CursorStore store; 25 | 26 | /** 27 | * Constructor 28 | * 29 | * @param store Cursor store to manage 30 | */ 31 | public CursorStoreManager(final CursorStore store) { 32 | this.store = Objects.requireNonNull(store, "store == null"); 33 | } 34 | 35 | @Override 36 | public void start() throws Exception { 37 | store.initialize(); 38 | } 39 | 40 | @Override 41 | public void stop() throws Exception { 42 | // nothing to stop 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/managed/NotificationStoreManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.managed; 17 | 18 | import com.smoketurner.notification.application.store.NotificationStore; 19 | import io.dropwizard.lifecycle.Managed; 20 | import java.util.Objects; 21 | 22 | public class NotificationStoreManager implements Managed { 23 | 24 | private final NotificationStore store; 25 | 26 | /** 27 | * Constructor 28 | * 29 | * @param store Notification store to manage 30 | */ 31 | public NotificationStoreManager(final NotificationStore store) { 32 | this.store = Objects.requireNonNull(store, "store == null"); 33 | } 34 | 35 | @Override 36 | public void start() throws Exception { 37 | store.initialize(); 38 | } 39 | 40 | @Override 41 | public void stop() throws Exception { 42 | // nothing to stop 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/resources/PingResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.resources; 17 | 18 | import io.dropwizard.jersey.caching.CacheControl; 19 | import javax.ws.rs.GET; 20 | import javax.ws.rs.Path; 21 | import javax.ws.rs.core.MediaType; 22 | import javax.ws.rs.core.Response; 23 | 24 | @Path("/ping") 25 | public class PingResource { 26 | 27 | @GET 28 | @CacheControl(mustRevalidate = true, noCache = true, noStore = true) 29 | public Response ping() { 30 | return Response.ok("pong").type(MediaType.TEXT_PLAIN).build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/resources/RuleResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.resources; 17 | 18 | import com.codahale.metrics.annotation.Timed; 19 | import com.smoketurner.notification.api.Rule; 20 | import com.smoketurner.notification.application.exceptions.NotificationException; 21 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 22 | import com.smoketurner.notification.application.store.RuleStore; 23 | import io.dropwizard.jersey.caching.CacheControl; 24 | import java.util.Map; 25 | import java.util.Objects; 26 | import java.util.Optional; 27 | import javax.validation.Valid; 28 | import javax.validation.constraints.NotNull; 29 | import javax.ws.rs.Consumes; 30 | import javax.ws.rs.DELETE; 31 | import javax.ws.rs.GET; 32 | import javax.ws.rs.PUT; 33 | import javax.ws.rs.Path; 34 | import javax.ws.rs.PathParam; 35 | import javax.ws.rs.Produces; 36 | import javax.ws.rs.core.MediaType; 37 | import javax.ws.rs.core.Response; 38 | import org.glassfish.jersey.server.JSONP; 39 | 40 | @Path("/v1/rules") 41 | public class RuleResource { 42 | 43 | private final RuleStore store; 44 | 45 | /** 46 | * Constructor 47 | * 48 | * @param store Rule data store 49 | */ 50 | public RuleResource(final RuleStore store) { 51 | this.store = Objects.requireNonNull(store); 52 | } 53 | 54 | @GET 55 | @JSONP 56 | @Timed 57 | @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) 58 | @CacheControl(mustRevalidate = true, noCache = true, noStore = true) 59 | public Response fetch() { 60 | 61 | final Optional> rules; 62 | try { 63 | rules = store.fetch(); 64 | } catch (NotificationStoreException e) { 65 | throw new NotificationException( 66 | Response.Status.INTERNAL_SERVER_ERROR, "Unable to fetch rules", e); 67 | } 68 | 69 | if (!rules.isPresent()) { 70 | throw new NotificationException(Response.Status.NOT_FOUND, "No rules found"); 71 | } 72 | 73 | return Response.ok(rules.get()).build(); 74 | } 75 | 76 | @PUT 77 | @Timed 78 | @Path("/{category}") 79 | @Consumes(MediaType.APPLICATION_JSON) 80 | @Produces(MediaType.APPLICATION_JSON) 81 | public Response store( 82 | @PathParam("category") final String category, @NotNull @Valid final Rule rule) { 83 | 84 | if (!rule.isValid()) { 85 | throw new NotificationException( 86 | Response.Status.BAD_REQUEST, 87 | "Rule must contain at least one of: max_size, max_duration, or match_on"); 88 | } 89 | 90 | try { 91 | store.store(category, rule); 92 | } catch (NotificationStoreException e) { 93 | throw new NotificationException( 94 | Response.Status.INTERNAL_SERVER_ERROR, "Unable to store rule", e); 95 | } 96 | 97 | return Response.noContent().build(); 98 | } 99 | 100 | @DELETE 101 | @Timed 102 | @Path("/{category}") 103 | public Response delete(@PathParam("category") final String category) { 104 | 105 | try { 106 | store.remove(category); 107 | } catch (NotificationStoreException e) { 108 | throw new NotificationException( 109 | Response.Status.INTERNAL_SERVER_ERROR, "Unable to remove rule", e); 110 | } 111 | return Response.noContent().build(); 112 | } 113 | 114 | @DELETE 115 | @Timed 116 | public Response delete() { 117 | 118 | try { 119 | store.removeAll(); 120 | } catch (NotificationStoreException e) { 121 | throw new NotificationException( 122 | Response.Status.INTERNAL_SERVER_ERROR, "Unable to remove rules", e); 123 | } 124 | return Response.noContent().build(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/resources/VersionResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.resources; 17 | 18 | import com.google.common.annotations.VisibleForTesting; 19 | import io.dropwizard.jersey.caching.CacheControl; 20 | import java.util.Objects; 21 | import javax.ws.rs.GET; 22 | import javax.ws.rs.Path; 23 | import javax.ws.rs.core.MediaType; 24 | import javax.ws.rs.core.Response; 25 | 26 | @Path("/version") 27 | public class VersionResource { 28 | 29 | private final String version; 30 | 31 | /** Constructor */ 32 | public VersionResource() { 33 | version = getClass().getPackage().getImplementationVersion(); 34 | } 35 | 36 | /** 37 | * Constructor 38 | * 39 | * @param version Version to expose in the endpoint 40 | */ 41 | @VisibleForTesting 42 | public VersionResource(final String version) { 43 | this.version = Objects.requireNonNull(version); 44 | } 45 | 46 | @GET 47 | @CacheControl(mustRevalidate = true, noCache = true, noStore = true) 48 | public Response getVersion() { 49 | return Response.ok(version).type(MediaType.TEXT_PLAIN).build(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/riak/CursorObject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import com.basho.riak.client.api.annotations.RiakBucketName; 19 | import com.basho.riak.client.api.annotations.RiakContentType; 20 | import com.basho.riak.client.api.annotations.RiakKey; 21 | import com.basho.riak.client.api.annotations.RiakLastModified; 22 | import com.basho.riak.client.api.annotations.RiakTombstone; 23 | import com.basho.riak.client.api.annotations.RiakVClock; 24 | import com.basho.riak.client.api.annotations.RiakVTag; 25 | import com.basho.riak.client.api.cap.VClock; 26 | import com.fasterxml.jackson.annotation.JsonCreator; 27 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 28 | import com.fasterxml.jackson.annotation.JsonInclude; 29 | import com.fasterxml.jackson.annotation.JsonProperty; 30 | import com.google.common.base.MoreObjects; 31 | import com.google.common.collect.ComparisonChain; 32 | import com.google.common.collect.Ordering; 33 | import java.util.Objects; 34 | import javax.annotation.Nullable; 35 | 36 | @JsonIgnoreProperties(ignoreUnknown = true) 37 | @JsonInclude(JsonInclude.Include.NON_NULL) 38 | public final class CursorObject implements Comparable { 39 | 40 | @RiakBucketName private final String bucketName = "cursors"; 41 | 42 | @RiakKey @Nullable private String key; 43 | 44 | @RiakVClock @Nullable private VClock vclock; 45 | 46 | @RiakTombstone @Nullable private Boolean tombstone; 47 | 48 | @RiakContentType @Nullable private String contentType; 49 | 50 | @RiakLastModified @Nullable private Long lastModified; 51 | 52 | @RiakVTag @Nullable private String vtag; 53 | 54 | @Nullable private String value; 55 | 56 | /** Constructor */ 57 | public CursorObject() { 58 | // needed to handle tombstones 59 | } 60 | 61 | /** 62 | * Constructor 63 | * 64 | * @param key Cursor key 65 | * @param value Cursor value 66 | */ 67 | @JsonCreator 68 | public CursorObject( 69 | @JsonProperty("key") final String key, @JsonProperty("value") final String value) { 70 | this.key = Objects.requireNonNull(key, "key == null"); 71 | this.value = value; 72 | } 73 | 74 | @Nullable 75 | @JsonProperty 76 | public String getKey() { 77 | return key; 78 | } 79 | 80 | @Nullable 81 | @JsonProperty 82 | public String getValue() { 83 | return value; 84 | } 85 | 86 | @JsonProperty 87 | public void setValue(final String value) { 88 | this.value = value; 89 | } 90 | 91 | @Override 92 | public boolean equals(final Object obj) { 93 | if (this == obj) { 94 | return true; 95 | } 96 | if ((obj == null) || (getClass() != obj.getClass())) { 97 | return false; 98 | } 99 | 100 | final CursorObject other = (CursorObject) obj; 101 | return Objects.equals(value, other.value); 102 | } 103 | 104 | @Override 105 | public int hashCode() { 106 | return Objects.hash(value); 107 | } 108 | 109 | @Override 110 | public String toString() { 111 | return MoreObjects.toStringHelper(this).add("key", key).add("value", value).toString(); 112 | } 113 | 114 | @Override 115 | public int compareTo(final CursorObject that) { 116 | return ComparisonChain.start() 117 | .compare(this.value, that.value, Ordering.natural().reverse()) 118 | .result(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/riak/CursorResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import static com.codahale.metrics.MetricRegistry.name; 19 | 20 | import com.basho.riak.client.api.cap.ConflictResolver; 21 | import com.basho.riak.client.api.cap.UnresolvedConflictException; 22 | import com.codahale.metrics.Histogram; 23 | import com.codahale.metrics.MetricRegistry; 24 | import com.codahale.metrics.SharedMetricRegistries; 25 | import java.util.Collections; 26 | import java.util.List; 27 | import javax.annotation.Nullable; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | public class CursorResolver implements ConflictResolver { 32 | 33 | private static final Logger LOGGER = LoggerFactory.getLogger(CursorResolver.class); 34 | private final Histogram siblingCounts; 35 | 36 | /** Constructor */ 37 | public CursorResolver() { 38 | final MetricRegistry registry = SharedMetricRegistries.getOrCreate("default"); 39 | this.siblingCounts = registry.histogram(name(CursorResolver.class, "sibling-counts")); 40 | } 41 | 42 | @Nullable 43 | @Override 44 | public CursorObject resolve(final List siblings) 45 | throws UnresolvedConflictException { 46 | LOGGER.debug("Found {} siblings", siblings.size()); 47 | siblingCounts.update(siblings.size()); 48 | if (siblings.size() > 1) { 49 | Collections.sort(siblings); 50 | return siblings.get(0); 51 | } else if (siblings.size() == 1) { 52 | return siblings.get(0); 53 | } else { 54 | return null; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/riak/CursorUpdate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import com.basho.riak.client.api.commands.kv.UpdateValue; 19 | import java.util.Objects; 20 | import javax.annotation.Nullable; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | public class CursorUpdate extends UpdateValue.Update { 25 | 26 | private static final Logger LOGGER = LoggerFactory.getLogger(CursorUpdate.class); 27 | private final String key; 28 | private final String value; 29 | 30 | /** 31 | * Constructor 32 | * 33 | * @param key Cursor key 34 | * @param value Cursor value 35 | */ 36 | public CursorUpdate(final String key, final String value) { 37 | this.key = Objects.requireNonNull(key, "key == null"); 38 | this.value = Objects.requireNonNull(value, "value == null"); 39 | } 40 | 41 | @Override 42 | public CursorObject apply(@Nullable CursorObject original) { 43 | if (original == null) { 44 | LOGGER.debug("original is null, creating new cursor"); 45 | original = new CursorObject(key, value); 46 | } else { 47 | original.setValue(value); 48 | } 49 | return original; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/riak/NotificationListAddition.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import com.basho.riak.client.api.commands.kv.UpdateValue; 19 | import com.smoketurner.notification.api.Notification; 20 | import java.util.Objects; 21 | import javax.annotation.Nullable; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | public class NotificationListAddition extends UpdateValue.Update { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(NotificationListAddition.class); 28 | private final Notification notification; 29 | 30 | /** 31 | * Constructor 32 | * 33 | * @param notification Notification to add 34 | */ 35 | public NotificationListAddition(final Notification notification) { 36 | this.notification = Objects.requireNonNull(notification, "notification == null"); 37 | } 38 | 39 | @Override 40 | public NotificationListObject apply(@Nullable NotificationListObject original) { 41 | if (original == null) { 42 | LOGGER.debug("original is null, creating new notification list"); 43 | original = new NotificationListObject(); 44 | } 45 | original.addNotification(notification); 46 | return original; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/riak/NotificationListConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import com.basho.riak.client.api.convert.ConversionException; 19 | import com.basho.riak.client.api.convert.Converter; 20 | import com.basho.riak.client.core.util.BinaryValue; 21 | import com.google.protobuf.InvalidProtocolBufferException; 22 | import com.smoketurner.notification.api.Notification; 23 | import com.smoketurner.notification.application.protos.NotificationProtos.NotificationListPB; 24 | import com.smoketurner.notification.application.protos.NotificationProtos.NotificationPB; 25 | import io.dropwizard.jersey.protobuf.ProtocolBufferMediaType; 26 | import java.time.Instant; 27 | import java.time.ZoneOffset; 28 | import java.time.ZonedDateTime; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | public class NotificationListConverter extends Converter { 33 | 34 | private static final Logger LOGGER = LoggerFactory.getLogger(NotificationListConverter.class); 35 | 36 | public NotificationListConverter() { 37 | super(NotificationListObject.class); 38 | } 39 | 40 | @Override 41 | public NotificationListObject toDomain(final BinaryValue value, final String contentType) { 42 | if (!ProtocolBufferMediaType.APPLICATION_PROTOBUF.equals(contentType)) { 43 | LOGGER.error("Invalid Content-Type: {}", contentType); 44 | throw new ConversionException("Invalid Content-Type: " + contentType); 45 | } 46 | 47 | final NotificationListPB list; 48 | try { 49 | list = NotificationListPB.parseFrom(value.unsafeGetValue()); 50 | } catch (InvalidProtocolBufferException e) { 51 | LOGGER.error("Unable to convert value from Riak", e); 52 | throw new ConversionException(e); 53 | } 54 | 55 | final NotificationListObject obj = new NotificationListObject(); 56 | list.getNotificationList().stream() 57 | .map(NotificationListConverter::convert) 58 | .forEach(obj::addNotification); 59 | obj.deleteNotifications(list.getDeletedIdList()); 60 | return obj; 61 | } 62 | 63 | @Override 64 | public ContentAndType fromDomain(final NotificationListObject domainObject) { 65 | final NotificationListPB.Builder builder = 66 | NotificationListPB.newBuilder().addAllDeletedId(domainObject.getDeletedIds()); 67 | 68 | domainObject.getNotifications().stream() 69 | .map(NotificationListConverter::convert) 70 | .forEach(builder::addNotification); 71 | 72 | final NotificationListPB list = builder.build(); 73 | 74 | return new ContentAndType( 75 | BinaryValue.unsafeCreate(list.toByteArray()), ProtocolBufferMediaType.APPLICATION_PROTOBUF); 76 | } 77 | 78 | private static Notification convert(final NotificationPB notification) { 79 | return Notification.builder(notification.getCategory(), notification.getMessage()) 80 | .withId(notification.getId()) 81 | .withCreatedAt( 82 | ZonedDateTime.ofInstant( 83 | Instant.ofEpochMilli(notification.getCreatedAt()), ZoneOffset.UTC)) 84 | .withProperties(notification.getPropertyMap()) 85 | .build(); 86 | } 87 | 88 | private static NotificationPB convert(final Notification notification) { 89 | return NotificationPB.newBuilder() 90 | .setId(notification.getId().get()) 91 | .setCategory(notification.getCategory()) 92 | .setMessage(notification.getMessage()) 93 | .setCreatedAt(notification.getCreatedAt().toInstant().toEpochMilli()) 94 | .putAllProperty(notification.getProperties()) 95 | .build(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/riak/NotificationListDeletion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import com.basho.riak.client.api.commands.kv.UpdateValue; 19 | import java.util.Collection; 20 | import java.util.Objects; 21 | import javax.annotation.Nullable; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | public class NotificationListDeletion extends UpdateValue.Update { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(NotificationListDeletion.class); 28 | private final Collection ids; 29 | 30 | /** 31 | * Constructor 32 | * 33 | * @param ids Notification IDs to delete 34 | */ 35 | public NotificationListDeletion(final Collection ids) { 36 | this.ids = Objects.requireNonNull(ids, "ids == null"); 37 | } 38 | 39 | @Override 40 | public NotificationListObject apply(@Nullable NotificationListObject original) { 41 | if (original == null) { 42 | LOGGER.debug("original is null, creating new notification list"); 43 | original = new NotificationListObject(); 44 | } 45 | original.deleteNotifications(ids); 46 | return original; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/riak/NotificationListObject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import com.basho.riak.client.api.annotations.RiakBucketName; 19 | import com.basho.riak.client.api.annotations.RiakContentType; 20 | import com.basho.riak.client.api.annotations.RiakKey; 21 | import com.basho.riak.client.api.annotations.RiakLastModified; 22 | import com.basho.riak.client.api.annotations.RiakTombstone; 23 | import com.basho.riak.client.api.annotations.RiakVClock; 24 | import com.basho.riak.client.api.annotations.RiakVTag; 25 | import com.basho.riak.client.api.cap.VClock; 26 | import com.google.common.base.MoreObjects; 27 | import com.smoketurner.notification.api.Notification; 28 | import java.util.Collection; 29 | import java.util.HashSet; 30 | import java.util.Objects; 31 | import java.util.Set; 32 | import java.util.SortedSet; 33 | import java.util.TreeSet; 34 | import javax.annotation.Nullable; 35 | 36 | public class NotificationListObject { 37 | 38 | private static final int MAX_NOTIFICATIONS = 1000; 39 | 40 | @RiakBucketName private final String bucketName = "notifications"; 41 | 42 | @RiakKey @Nullable private String key; 43 | 44 | @RiakVClock @Nullable private VClock vclock; 45 | 46 | @RiakTombstone @Nullable private Boolean tombstone; 47 | 48 | @RiakContentType @Nullable private String contentType; 49 | 50 | @RiakLastModified @Nullable private Long lastModified; 51 | 52 | @RiakVTag @Nullable private String vtag; 53 | 54 | private final TreeSet notifications = new TreeSet<>(); 55 | private final Set deletedIds = new HashSet<>(); 56 | 57 | /** Constructor */ 58 | public NotificationListObject() { 59 | // needed to handle tombstones 60 | } 61 | 62 | /** 63 | * Constructor 64 | * 65 | * @param key Notification key 66 | */ 67 | public NotificationListObject(final String key) { 68 | this.key = Objects.requireNonNull(key, "key == null"); 69 | } 70 | 71 | public void addNotification(final Notification notification) { 72 | notifications.add(notification); 73 | if (notifications.size() > MAX_NOTIFICATIONS) { 74 | notifications.pollLast(); 75 | } 76 | } 77 | 78 | public void addNotifications(final Collection notifications) { 79 | this.notifications.addAll(notifications); 80 | while (this.notifications.size() > MAX_NOTIFICATIONS) { 81 | this.notifications.pollLast(); 82 | } 83 | } 84 | 85 | public void deleteNotification(final String id) { 86 | deletedIds.add(id); 87 | } 88 | 89 | public void deleteNotifications(final Collection ids) { 90 | deletedIds.addAll(ids); 91 | } 92 | 93 | @Nullable 94 | public String getKey() { 95 | return key; 96 | } 97 | 98 | public SortedSet getNotifications() { 99 | return notifications; 100 | } 101 | 102 | public Set getDeletedIds() { 103 | return deletedIds; 104 | } 105 | 106 | @Override 107 | public boolean equals(final Object obj) { 108 | if (this == obj) { 109 | return true; 110 | } 111 | if ((obj == null) || (getClass() != obj.getClass())) { 112 | return false; 113 | } 114 | 115 | final NotificationListObject other = (NotificationListObject) obj; 116 | return Objects.equals(key, other.key) 117 | && Objects.equals(vclock, other.vclock) 118 | && Objects.equals(tombstone, other.tombstone) 119 | && Objects.equals(contentType, other.contentType) 120 | && Objects.equals(lastModified, other.lastModified) 121 | && Objects.equals(vtag, other.vtag) 122 | && Objects.equals(notifications, other.notifications) 123 | && Objects.equals(deletedIds, other.deletedIds); 124 | } 125 | 126 | @Override 127 | public int hashCode() { 128 | return Objects.hash( 129 | key, vclock, tombstone, contentType, lastModified, vtag, notifications, deletedIds); 130 | } 131 | 132 | @Override 133 | public String toString() { 134 | return MoreObjects.toStringHelper(this) 135 | .add("key", key) 136 | .add("vclock", vclock) 137 | .add("tombstone", tombstone) 138 | .add("contentType", contentType) 139 | .add("lastModified", lastModified) 140 | .add("vtag", vtag) 141 | .add("notifications", notifications) 142 | .add("deletedIds", deletedIds) 143 | .toString(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /notification-application/src/main/java/com/smoketurner/notification/application/riak/NotificationListResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import static com.codahale.metrics.MetricRegistry.name; 19 | 20 | import com.basho.riak.client.api.cap.ConflictResolver; 21 | import com.basho.riak.client.api.cap.UnresolvedConflictException; 22 | import com.codahale.metrics.Histogram; 23 | import com.codahale.metrics.MetricRegistry; 24 | import com.codahale.metrics.SharedMetricRegistries; 25 | import com.smoketurner.notification.api.Notification; 26 | import java.util.Collection; 27 | import java.util.Iterator; 28 | import java.util.List; 29 | import java.util.Set; 30 | import javax.annotation.Nullable; 31 | import org.slf4j.Logger; 32 | import org.slf4j.LoggerFactory; 33 | 34 | public class NotificationListResolver implements ConflictResolver { 35 | 36 | private static final Logger LOGGER = LoggerFactory.getLogger(NotificationListResolver.class); 37 | private final Histogram siblingCounts; 38 | 39 | /** Constructor */ 40 | public NotificationListResolver() { 41 | final MetricRegistry registry = SharedMetricRegistries.getOrCreate("default"); 42 | this.siblingCounts = registry.histogram(name(NotificationListResolver.class, "sibling-counts")); 43 | } 44 | 45 | @Override 46 | @Nullable 47 | public NotificationListObject resolve(final List siblings) 48 | throws UnresolvedConflictException { 49 | 50 | LOGGER.debug("Found {} siblings", siblings.size()); 51 | siblingCounts.update(siblings.size()); 52 | if (siblings.size() > 1) { 53 | 54 | final Iterator iterator = siblings.iterator(); 55 | final NotificationListObject resolved = iterator.next(); 56 | final Set deletedIds = resolved.getDeletedIds(); 57 | 58 | // add all notifications 59 | while (iterator.hasNext()) { 60 | final NotificationListObject sibling = iterator.next(); 61 | resolved.addNotifications(sibling.getNotifications()); 62 | deletedIds.addAll(sibling.getDeletedIds()); 63 | } 64 | 65 | // remove deleted notifications 66 | if (!deletedIds.isEmpty()) { 67 | LOGGER.debug("IDs to delete: {}", deletedIds); 68 | removeNotifications(resolved.getNotifications(), deletedIds); 69 | } 70 | 71 | return resolved; 72 | } else if (siblings.size() == 1) { 73 | 74 | final NotificationListObject resolved = siblings.get(0); 75 | 76 | // remove deleted notifications 77 | if (!resolved.getDeletedIds().isEmpty()) { 78 | LOGGER.debug("IDs to delete: {}", resolved.getDeletedIds()); 79 | removeNotifications(resolved.getNotifications(), resolved.getDeletedIds()); 80 | } 81 | 82 | return resolved; 83 | } else { 84 | return null; 85 | } 86 | } 87 | 88 | /** 89 | * Remove the given notification IDs from the list of notifications. 90 | * 91 | * @param notifications Notifications to delete from 92 | * @param ids Notification IDs to delete 93 | */ 94 | public static void removeNotifications( 95 | final Collection notifications, final Collection ids) { 96 | 97 | notifications.removeIf( 98 | notification -> { 99 | if (!notification.getId().isPresent()) { 100 | return true; 101 | } 102 | return ids.contains(notification.getId().get()); 103 | }); 104 | // clear out the original set of IDs to delete 105 | ids.clear(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /notification-application/src/main/proto/notification.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package notification; 4 | 5 | option java_package = "com.smoketurner.notification.application.protos"; 6 | option java_outer_classname = "NotificationProtos"; 7 | option optimize_for = SPEED; 8 | 9 | message NotificationPB { 10 | string id = 1; 11 | string category = 2; 12 | string message = 3; 13 | int64 created_at = 4; 14 | map property = 5; 15 | }; 16 | 17 | message NotificationListPB { 18 | repeated NotificationPB notification = 1; 19 | repeated string deleted_id = 2; 20 | }; -------------------------------------------------------------------------------- /notification-application/src/main/resources/Notification.graphql: -------------------------------------------------------------------------------- 1 | scalar Map 2 | 3 | schema { 4 | query: Query 5 | mutation: Mutation 6 | } 7 | 8 | type Query { 9 | notifications(username: String!): [Notification!] 10 | rules: [RuleCategory!] 11 | } 12 | 13 | type Mutation { 14 | createNotification(username: String!, notification: NotificationInput!): Notification 15 | createRule(category: String!, rule: RuleInput!): Boolean! 16 | removeAllNotifications(username: String!): Boolean! 17 | removeAllRules: Boolean! 18 | removeNotification(username: String!, ids: [ID!]!): Boolean! 19 | removeRule(category: String!): Boolean! 20 | } 21 | 22 | type Notification { 23 | id: ID! 24 | category: String! 25 | message: String! 26 | unseen: Boolean! 27 | createdAt: String! 28 | properties: Map! 29 | notifications: [Notification!] 30 | } 31 | 32 | type RuleCategory { 33 | category: String! 34 | rule: Rule! 35 | } 36 | 37 | type Rule { 38 | maxSize: Int 39 | maxDuration: String 40 | matchOn: String 41 | } 42 | 43 | input NotificationInput { 44 | category: String! 45 | message: String! 46 | properties: Map 47 | } 48 | 49 | input RuleInput { 50 | maxSize: Int 51 | maxDuration: String 52 | matchOn: String 53 | } 54 | -------------------------------------------------------------------------------- /notification-application/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ _ __ _ _ _ 2 | | \ | | | | (_)/ _(_) | | (_) 3 | | \| | ___ | |_ _| |_ _ ___ __ _| |_ _ ___ _ __ 4 | | . ` |/ _ \| __| | _| |/ __/ _` | __| |/ _ \| '_ \ 5 | | |\ | (_) | |_| | | | | (_| (_| | |_| | (_) | | | | 6 | |_| \_|\___/ \__|_|_| |_|\___\__,_|\__|_|\___/|_| |_| 7 | 8 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/NotificationApplicationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application; 17 | 18 | import static org.mockito.ArgumentMatchers.isA; 19 | import static org.mockito.Mockito.mock; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.when; 22 | 23 | import com.codahale.metrics.MetricRegistry; 24 | import com.codahale.metrics.health.HealthCheckRegistry; 25 | import com.fasterxml.jackson.databind.ObjectMapper; 26 | import com.smoketurner.notification.application.config.NotificationConfiguration; 27 | import com.smoketurner.notification.application.resources.NotificationResource; 28 | import com.smoketurner.notification.application.resources.PingResource; 29 | import com.smoketurner.notification.application.resources.VersionResource; 30 | import io.dropwizard.jackson.Jackson; 31 | import io.dropwizard.jersey.setup.JerseyEnvironment; 32 | import io.dropwizard.lifecycle.setup.LifecycleEnvironment; 33 | import io.dropwizard.setup.Environment; 34 | import org.junit.Before; 35 | import org.junit.Test; 36 | 37 | public class NotificationApplicationTest { 38 | private final MetricRegistry registry = new MetricRegistry(); 39 | private final ObjectMapper mapper = Jackson.newObjectMapper(); 40 | private final Environment environment = mock(Environment.class); 41 | private final JerseyEnvironment jersey = mock(JerseyEnvironment.class); 42 | private final LifecycleEnvironment lifecycle = mock(LifecycleEnvironment.class); 43 | private final HealthCheckRegistry healthChecks = mock(HealthCheckRegistry.class); 44 | private final NotificationApplication application = new NotificationApplication(); 45 | private final NotificationConfiguration config = new NotificationConfiguration(); 46 | 47 | @Before 48 | public void setup() throws Exception { 49 | when(environment.metrics()).thenReturn(registry); 50 | when(environment.jersey()).thenReturn(jersey); 51 | when(environment.getObjectMapper()).thenReturn(mapper); 52 | when(environment.lifecycle()).thenReturn(lifecycle); 53 | when(environment.healthChecks()).thenReturn(healthChecks); 54 | } 55 | 56 | @Test 57 | public void buildsAVersionResource() throws Exception { 58 | application.run(config, environment); 59 | verify(jersey).register(isA(VersionResource.class)); 60 | } 61 | 62 | @Test 63 | public void buildsAPingResource() throws Exception { 64 | application.run(config, environment); 65 | verify(jersey).register(isA(PingResource.class)); 66 | } 67 | 68 | @Test 69 | public void buildsANotificationResource() throws Exception { 70 | application.run(config, environment); 71 | verify(jersey).register(isA(NotificationResource.class)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/benchmarks/NotificationStoreBenchmark.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.benchmarks; 17 | 18 | import com.smoketurner.notification.api.Notification; 19 | import com.smoketurner.notification.application.store.NotificationStore; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.Optional; 23 | import java.util.concurrent.TimeUnit; 24 | import java.util.stream.Stream; 25 | import org.openjdk.jmh.annotations.Benchmark; 26 | import org.openjdk.jmh.annotations.BenchmarkMode; 27 | import org.openjdk.jmh.annotations.Mode; 28 | import org.openjdk.jmh.annotations.OutputTimeUnit; 29 | import org.openjdk.jmh.annotations.Scope; 30 | import org.openjdk.jmh.annotations.Setup; 31 | import org.openjdk.jmh.annotations.State; 32 | import org.openjdk.jmh.runner.Runner; 33 | import org.openjdk.jmh.runner.options.OptionsBuilder; 34 | 35 | @BenchmarkMode(Mode.AverageTime) 36 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 37 | @State(Scope.Benchmark) 38 | public class NotificationStoreBenchmark { 39 | 40 | private final List notifications = new ArrayList<>(100000); 41 | 42 | @Setup 43 | public void setUp() { 44 | for (int i = 0; i < 100000; i++) { 45 | notifications.add(Notification.create(String.format("%05d", i))); 46 | } 47 | } 48 | 49 | @Benchmark 50 | public Stream setUnseenState() { 51 | return NotificationStore.setUnseenState(notifications, true); 52 | } 53 | 54 | @Benchmark 55 | public Optional tryFind() { 56 | return NotificationStore.tryFind(notifications, "10000"); 57 | } 58 | 59 | @Benchmark 60 | public int indexOf() { 61 | return NotificationStore.indexOf(notifications, "10000"); 62 | } 63 | 64 | public static void main(String[] args) throws Exception { 65 | new Runner( 66 | new OptionsBuilder() 67 | .include(NotificationStoreBenchmark.class.getSimpleName()) 68 | .forks(1) 69 | .warmupIterations(5) 70 | .measurementIterations(5) 71 | .build()) 72 | .run(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/benchmarks/RollupBenchmark.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.benchmarks; 17 | 18 | import com.google.common.collect.ImmutableMap; 19 | import com.smoketurner.notification.api.Notification; 20 | import com.smoketurner.notification.api.Rule; 21 | import com.smoketurner.notification.application.core.Rollup; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | import java.util.concurrent.TimeUnit; 25 | import java.util.stream.Stream; 26 | import org.openjdk.jmh.annotations.Benchmark; 27 | import org.openjdk.jmh.annotations.BenchmarkMode; 28 | import org.openjdk.jmh.annotations.Mode; 29 | import org.openjdk.jmh.annotations.OutputTimeUnit; 30 | import org.openjdk.jmh.annotations.Scope; 31 | import org.openjdk.jmh.annotations.Setup; 32 | import org.openjdk.jmh.annotations.State; 33 | import org.openjdk.jmh.runner.Runner; 34 | import org.openjdk.jmh.runner.options.OptionsBuilder; 35 | 36 | @BenchmarkMode(Mode.AverageTime) 37 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 38 | @State(Scope.Benchmark) 39 | public class RollupBenchmark { 40 | 41 | private static final String CATEGORY = "test"; 42 | private final Rule sizeRule = Rule.builder().withMaxSize(3).build(); 43 | private final List notifications = new ArrayList<>(1000); 44 | 45 | @Setup 46 | public void setUp() { 47 | for (int i = 0; i < 1000; i++) { 48 | notifications.add(Notification.builder(CATEGORY).withId(String.format("%03d", i)).build()); 49 | } 50 | } 51 | 52 | @Benchmark 53 | public Stream rollupNoRules() { 54 | final Rollup rollup = new Rollup(ImmutableMap.of()); 55 | return rollup.rollup(notifications.stream()); 56 | } 57 | 58 | @Benchmark 59 | public Stream rollupNoMatches() { 60 | final Rollup rollup = new Rollup(ImmutableMap.of("other", sizeRule)); 61 | return rollup.rollup(notifications.stream()); 62 | } 63 | 64 | @Benchmark 65 | public Stream rollupEveryMatch() { 66 | final Rollup rollup = new Rollup(ImmutableMap.of(CATEGORY, sizeRule)); 67 | return rollup.rollup(notifications.stream()); 68 | } 69 | 70 | public static void main(String[] args) throws Exception { 71 | new Runner( 72 | new OptionsBuilder() 73 | .include(RollupBenchmark.class.getSimpleName()) 74 | .forks(1) 75 | .warmupIterations(5) 76 | .measurementIterations(5) 77 | .build()) 78 | .run(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/core/StringSetParamTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.core; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import com.google.common.collect.ImmutableSet; 21 | import java.util.Set; 22 | import org.junit.Test; 23 | 24 | public class StringSetParamTest { 25 | 26 | @Test 27 | public void testParse() throws Exception { 28 | final StringSetParam param = new StringSetParam("1,3, 2 ,asdf, 3"); 29 | final Set expected = ImmutableSet.of("1", "2", "3"); 30 | assertThat(param.parse("1,2,asdf,3")).containsAll(expected); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/core/UserNotificationsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.core; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import com.smoketurner.notification.api.Notification; 21 | import java.util.Arrays; 22 | import java.util.Collections; 23 | import java.util.List; 24 | import org.junit.Test; 25 | 26 | public class UserNotificationsTest { 27 | 28 | @Test 29 | public void testIsEmpty() { 30 | final UserNotifications notifications = new UserNotifications(); 31 | assertThat(notifications.isEmpty()).isTrue(); 32 | assertThat(notifications.getUnseen()).isEmpty(); 33 | assertThat(notifications.getSeen()).isEmpty(); 34 | } 35 | 36 | @Test 37 | public void testUnseen() { 38 | final List unseen = Collections.singletonList(Notification.create("1")); 39 | final UserNotifications notifications = new UserNotifications(unseen); 40 | assertThat(notifications.isEmpty()).isFalse(); 41 | assertThat(notifications.getUnseen()).containsExactlyElementsOf(unseen); 42 | assertThat(notifications.getSeen()).isEmpty(); 43 | } 44 | 45 | @Test 46 | public void testSeenUnseen() { 47 | final List unseen = Collections.singletonList(Notification.create("2")); 48 | final List seen = Collections.singletonList(Notification.create("1")); 49 | final List expected = 50 | Arrays.asList(Notification.create("2"), Notification.create("1")); 51 | final UserNotifications notifications = new UserNotifications(unseen, seen); 52 | assertThat(notifications.isEmpty()).isFalse(); 53 | assertThat(notifications.getUnseen()).containsExactlyElementsOf(unseen); 54 | assertThat(notifications.getSeen()).containsExactlyElementsOf(seen); 55 | assertThat(notifications.getNotifications()).containsExactlyElementsOf(expected); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/graphql/NotificationDataFetcherTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.mockito.ArgumentMatchers.anyString; 20 | import static org.mockito.ArgumentMatchers.eq; 21 | import static org.mockito.Mockito.doThrow; 22 | import static org.mockito.Mockito.mock; 23 | import static org.mockito.Mockito.never; 24 | import static org.mockito.Mockito.verify; 25 | import static org.mockito.Mockito.when; 26 | 27 | import com.smoketurner.notification.api.Notification; 28 | import com.smoketurner.notification.application.core.UserNotifications; 29 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 30 | import com.smoketurner.notification.application.store.NotificationStore; 31 | import graphql.schema.DataFetchingEnvironment; 32 | import java.util.Optional; 33 | import java.util.SortedSet; 34 | import java.util.TreeSet; 35 | import org.junit.Test; 36 | 37 | public class NotificationDataFetcherTest { 38 | 39 | private final NotificationStore store = mock(NotificationStore.class); 40 | private final DataFetchingEnvironment environment = mock(DataFetchingEnvironment.class); 41 | private final NotificationDataFetcher fetcher = new NotificationDataFetcher(store); 42 | 43 | @Test 44 | public void testUsernameNull() throws Exception { 45 | when(environment.getArgument("username")).thenReturn(null); 46 | 47 | final SortedSet actual = fetcher.get(environment); 48 | 49 | assertThat(actual).isNull(); 50 | 51 | verify(store, never()).fetch(anyString()); 52 | } 53 | 54 | @Test 55 | public void testUsernameEmpty() throws Exception { 56 | when(environment.getArgument("username")).thenReturn(""); 57 | 58 | final SortedSet actual = fetcher.get(environment); 59 | 60 | assertThat(actual).isNull(); 61 | 62 | verify(store, never()).fetch(anyString()); 63 | } 64 | 65 | @Test 66 | public void testStoreException() throws Exception { 67 | when(environment.getArgument("username")).thenReturn("test"); 68 | doThrow(new NotificationStoreException()).when(store).fetch(anyString()); 69 | 70 | final SortedSet actual = fetcher.get(environment); 71 | 72 | assertThat(actual).isNull(); 73 | 74 | verify(store).fetch(eq("test")); 75 | } 76 | 77 | @Test 78 | public void testNoNotifications() throws Exception { 79 | when(environment.getArgument("username")).thenReturn("test"); 80 | 81 | when(store.fetch(anyString())).thenReturn(Optional.empty()); 82 | 83 | final SortedSet actual = fetcher.get(environment); 84 | verify(store).fetch(eq("test")); 85 | 86 | assertThat(actual).isNotNull(); 87 | assertThat(actual).isEmpty(); 88 | } 89 | 90 | @Test 91 | public void testFetchNotifications() throws Exception { 92 | when(environment.getArgument("username")).thenReturn("test"); 93 | 94 | final Notification n1 = Notification.create("1"); 95 | final Notification n2 = Notification.create("2"); 96 | final Notification n3 = Notification.create("3"); 97 | final Notification n4 = Notification.create("4"); 98 | 99 | final TreeSet set = new TreeSet<>(); 100 | set.add(n1); 101 | set.add(n2); 102 | set.add(n3); 103 | set.add(n4); 104 | 105 | final UserNotifications notifications = new UserNotifications(set); 106 | 107 | when(store.fetch(anyString())).thenReturn(Optional.of(notifications)); 108 | 109 | final SortedSet actual = fetcher.get(environment); 110 | verify(store).fetch(eq("test")); 111 | 112 | assertThat(actual).isNotNull(); 113 | assertThat(actual.first()).isEqualTo(n4); 114 | assertThat(actual.last()).isEqualTo(n1); 115 | assertThat(actual.size()).isEqualTo(4); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/graphql/RemoveAllNotificationsMutationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; 20 | import static org.mockito.ArgumentMatchers.anyString; 21 | import static org.mockito.ArgumentMatchers.eq; 22 | import static org.mockito.Mockito.doThrow; 23 | import static org.mockito.Mockito.mock; 24 | import static org.mockito.Mockito.never; 25 | import static org.mockito.Mockito.verify; 26 | import static org.mockito.Mockito.when; 27 | 28 | import com.smoketurner.dropwizard.graphql.GraphQLValidationError; 29 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 30 | import com.smoketurner.notification.application.store.NotificationStore; 31 | import graphql.schema.DataFetchingEnvironment; 32 | import org.junit.Test; 33 | 34 | public class RemoveAllNotificationsMutationTest { 35 | 36 | private final NotificationStore store = mock(NotificationStore.class); 37 | private final DataFetchingEnvironment environment = mock(DataFetchingEnvironment.class); 38 | private final RemoveAllNotificationsMutation mutation = new RemoveAllNotificationsMutation(store); 39 | 40 | @Test 41 | public void testUsernameNull() throws Exception { 42 | when(environment.getArgument("username")).thenReturn(null); 43 | 44 | try { 45 | mutation.get(environment); 46 | failBecauseExceptionWasNotThrown(GraphQLValidationError.class); 47 | } catch (GraphQLValidationError e) { 48 | assertThat(e.getMessage()).isEqualTo("username cannot be empty"); 49 | } 50 | 51 | verify(store, never()).removeAll(anyString()); 52 | } 53 | 54 | @Test 55 | public void testUsernameEmpty() throws Exception { 56 | when(environment.getArgument("username")).thenReturn(""); 57 | 58 | try { 59 | mutation.get(environment); 60 | failBecauseExceptionWasNotThrown(GraphQLValidationError.class); 61 | } catch (GraphQLValidationError e) { 62 | assertThat(e.getMessage()).isEqualTo("username cannot be empty"); 63 | } 64 | 65 | verify(store, never()).removeAll(anyString()); 66 | } 67 | 68 | @Test 69 | public void testStoreException() throws Exception { 70 | when(environment.getArgument("username")).thenReturn("test"); 71 | doThrow(new NotificationStoreException()).when(store).removeAll(anyString()); 72 | 73 | try { 74 | mutation.get(environment); 75 | failBecauseExceptionWasNotThrown(GraphQLValidationError.class); 76 | } catch (GraphQLValidationError e) { 77 | assertThat(e.getMessage()).isEqualTo("Unable to remove all notifications"); 78 | } 79 | 80 | verify(store).removeAll(eq("test")); 81 | } 82 | 83 | @Test 84 | public void testRemoveAllNotifications() throws Exception { 85 | when(environment.getArgument("username")).thenReturn("test"); 86 | 87 | final Boolean actual = mutation.get(environment); 88 | verify(store).removeAll(eq("test")); 89 | 90 | assertThat(actual).isTrue(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/graphql/RemoveAllRulesMutationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; 20 | import static org.mockito.Mockito.doThrow; 21 | import static org.mockito.Mockito.mock; 22 | import static org.mockito.Mockito.verify; 23 | 24 | import com.smoketurner.dropwizard.graphql.GraphQLValidationError; 25 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 26 | import com.smoketurner.notification.application.store.RuleStore; 27 | import graphql.schema.DataFetchingEnvironment; 28 | import org.junit.Test; 29 | 30 | public class RemoveAllRulesMutationTest { 31 | 32 | private final RuleStore store = mock(RuleStore.class); 33 | private final DataFetchingEnvironment environment = mock(DataFetchingEnvironment.class); 34 | private final RemoveAllRulesMutation mutation = new RemoveAllRulesMutation(store); 35 | 36 | @Test 37 | public void testStoreException() throws Exception { 38 | doThrow(new NotificationStoreException()).when(store).removeAll(); 39 | 40 | try { 41 | mutation.get(environment); 42 | failBecauseExceptionWasNotThrown(GraphQLValidationError.class); 43 | } catch (GraphQLValidationError e) { 44 | assertThat(e.getMessage()).isEqualTo("Unable to remove all rules"); 45 | } 46 | 47 | verify(store).removeAll(); 48 | } 49 | 50 | @Test 51 | public void testRemoveAllRules() throws Exception { 52 | final Boolean actual = mutation.get(environment); 53 | verify(store).removeAll(); 54 | assertThat(actual).isTrue(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/graphql/RemoveNotificationMutationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; 20 | import static org.mockito.ArgumentMatchers.anyList; 21 | import static org.mockito.ArgumentMatchers.anyString; 22 | import static org.mockito.ArgumentMatchers.eq; 23 | import static org.mockito.Mockito.doThrow; 24 | import static org.mockito.Mockito.mock; 25 | import static org.mockito.Mockito.never; 26 | import static org.mockito.Mockito.verify; 27 | import static org.mockito.Mockito.when; 28 | 29 | import com.smoketurner.dropwizard.graphql.GraphQLValidationError; 30 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 31 | import com.smoketurner.notification.application.store.NotificationStore; 32 | import graphql.schema.DataFetchingEnvironment; 33 | import java.util.Collections; 34 | import org.junit.Test; 35 | 36 | public class RemoveNotificationMutationTest { 37 | 38 | private final NotificationStore store = mock(NotificationStore.class); 39 | private final DataFetchingEnvironment environment = mock(DataFetchingEnvironment.class); 40 | private final RemoveNotificationMutation mutation = new RemoveNotificationMutation(store); 41 | 42 | @Test 43 | public void testUsernameNull() throws Exception { 44 | when(environment.getArgument("username")).thenReturn(null); 45 | 46 | try { 47 | mutation.get(environment); 48 | failBecauseExceptionWasNotThrown(GraphQLValidationError.class); 49 | } catch (GraphQLValidationError e) { 50 | assertThat(e.getMessage()).isEqualTo("username cannot be empty"); 51 | } 52 | 53 | verify(store, never()).remove(anyString(), anyList()); 54 | } 55 | 56 | @Test 57 | public void testUsernameEmpty() throws Exception { 58 | when(environment.getArgument("username")).thenReturn(""); 59 | 60 | try { 61 | mutation.get(environment); 62 | failBecauseExceptionWasNotThrown(GraphQLValidationError.class); 63 | } catch (GraphQLValidationError e) { 64 | assertThat(e.getMessage()).isEqualTo("username cannot be empty"); 65 | } 66 | 67 | verify(store, never()).remove(anyString(), anyList()); 68 | } 69 | 70 | @Test 71 | public void testIdsNull() throws Exception { 72 | when(environment.getArgument("username")).thenReturn("test"); 73 | when(environment.getArgument("ids")).thenReturn(null); 74 | 75 | final Boolean actual = mutation.get(environment); 76 | verify(store, never()).remove(anyString(), anyList()); 77 | 78 | assertThat(actual).isFalse(); 79 | } 80 | 81 | @Test 82 | public void testIdsEmpty() throws Exception { 83 | when(environment.getArgument("username")).thenReturn("test"); 84 | when(environment.getArgument("ids")).thenReturn(Collections.emptyList()); 85 | 86 | final Boolean actual = mutation.get(environment); 87 | verify(store, never()).remove(anyString(), anyList()); 88 | 89 | assertThat(actual).isFalse(); 90 | } 91 | 92 | @Test 93 | public void testStoreException() throws Exception { 94 | when(environment.getArgument("username")).thenReturn("test"); 95 | when(environment.getArgument("ids")).thenReturn(Collections.singletonList("1")); 96 | doThrow(new NotificationStoreException()).when(store).remove(anyString(), anyList()); 97 | 98 | try { 99 | mutation.get(environment); 100 | failBecauseExceptionWasNotThrown(GraphQLValidationError.class); 101 | } catch (GraphQLValidationError e) { 102 | assertThat(e.getMessage()).isEqualTo("Unable to remove notifications"); 103 | } 104 | 105 | verify(store).remove(eq("test"), eq(Collections.singletonList("1"))); 106 | } 107 | 108 | @Test 109 | public void testRemoveNotification() throws Exception { 110 | when(environment.getArgument("username")).thenReturn("test"); 111 | when(environment.getArgument("ids")).thenReturn(Collections.singletonList("1")); 112 | 113 | final Boolean actual = mutation.get(environment); 114 | verify(store).remove(eq("test"), eq(Collections.singletonList("1"))); 115 | 116 | assertThat(actual).isTrue(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/graphql/RemoveRuleMutationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; 20 | import static org.mockito.ArgumentMatchers.anyString; 21 | import static org.mockito.ArgumentMatchers.eq; 22 | import static org.mockito.Mockito.doThrow; 23 | import static org.mockito.Mockito.mock; 24 | import static org.mockito.Mockito.never; 25 | import static org.mockito.Mockito.verify; 26 | import static org.mockito.Mockito.when; 27 | 28 | import com.smoketurner.dropwizard.graphql.GraphQLValidationError; 29 | import com.smoketurner.notification.application.exceptions.NotificationStoreException; 30 | import com.smoketurner.notification.application.store.RuleStore; 31 | import graphql.schema.DataFetchingEnvironment; 32 | import org.junit.Test; 33 | 34 | public class RemoveRuleMutationTest { 35 | 36 | private final RuleStore store = mock(RuleStore.class); 37 | private final DataFetchingEnvironment environment = mock(DataFetchingEnvironment.class); 38 | private final RemoveRuleMutation mutation = new RemoveRuleMutation(store); 39 | 40 | @Test 41 | public void testCategoryNull() throws Exception { 42 | when(environment.getArgument("category")).thenReturn(null); 43 | 44 | try { 45 | mutation.get(environment); 46 | failBecauseExceptionWasNotThrown(GraphQLValidationError.class); 47 | } catch (GraphQLValidationError e) { 48 | assertThat(e.getMessage()).isEqualTo("category cannot be empty"); 49 | } 50 | 51 | verify(store, never()).remove(anyString()); 52 | } 53 | 54 | @Test 55 | public void testCategoryEmpty() throws Exception { 56 | when(environment.getArgument("category")).thenReturn(""); 57 | 58 | try { 59 | mutation.get(environment); 60 | failBecauseExceptionWasNotThrown(GraphQLValidationError.class); 61 | } catch (GraphQLValidationError e) { 62 | assertThat(e.getMessage()).isEqualTo("category cannot be empty"); 63 | } 64 | 65 | verify(store, never()).remove(anyString()); 66 | } 67 | 68 | @Test 69 | public void testStoreException() throws Exception { 70 | when(environment.getArgument("category")).thenReturn("like"); 71 | doThrow(new NotificationStoreException()).when(store).remove(anyString()); 72 | 73 | try { 74 | mutation.get(environment); 75 | failBecauseExceptionWasNotThrown(GraphQLValidationError.class); 76 | } catch (GraphQLValidationError e) { 77 | assertThat(e.getMessage()).isEqualTo("Unable to remove rule"); 78 | } 79 | 80 | verify(store).remove(eq("like")); 81 | } 82 | 83 | @Test 84 | public void testRemoveRule() throws Exception { 85 | when(environment.getArgument("category")).thenReturn("like"); 86 | 87 | final Boolean actual = mutation.get(environment); 88 | verify(store).remove(eq("like")); 89 | 90 | assertThat(actual).isTrue(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/graphql/RuleDataFetcherTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.graphql; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.mockito.Mockito.mock; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.when; 22 | 23 | import com.smoketurner.notification.api.Rule; 24 | import com.smoketurner.notification.application.store.RuleStore; 25 | import graphql.schema.DataFetchingEnvironment; 26 | import java.util.Collections; 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | import org.junit.Test; 31 | 32 | public class RuleDataFetcherTest { 33 | 34 | private final RuleStore store = mock(RuleStore.class); 35 | private final DataFetchingEnvironment environment = mock(DataFetchingEnvironment.class); 36 | private final RuleDataFetcher fetcher = new RuleDataFetcher(store); 37 | 38 | @Test 39 | public void testEmptyRules() throws Exception { 40 | when(store.fetchCached()).thenReturn(Collections.emptyMap()); 41 | 42 | final List> actual = fetcher.get(environment); 43 | verify(store).fetchCached(); 44 | 45 | assertThat(actual).isNotNull(); 46 | assertThat(actual).isEmpty(); 47 | } 48 | 49 | @Test 50 | public void testFetchRules() throws Exception { 51 | final Rule expected = Rule.builder().withMaxSize(3).build(); 52 | final Map rules = new HashMap<>(); 53 | rules.put("like", expected); 54 | 55 | when(store.fetchCached()).thenReturn(rules); 56 | 57 | final List> actual = fetcher.get(environment); 58 | verify(store).fetchCached(); 59 | 60 | assertThat(actual).isNotNull(); 61 | assertThat(actual.get(0).get("category")).isEqualTo("like"); 62 | assertThat(actual.get(0).get("rule")).isEqualTo(expected); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/integration/NotificationsIT.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.integration; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import com.google.common.collect.ImmutableList; 21 | import com.google.common.collect.Sets; 22 | import com.smoketurner.notification.api.Notification; 23 | import com.smoketurner.notification.application.NotificationApplication; 24 | import com.smoketurner.notification.application.config.NotificationConfiguration; 25 | import io.dropwizard.client.JerseyClientBuilder; 26 | import io.dropwizard.testing.ResourceHelpers; 27 | import io.dropwizard.testing.junit.DropwizardAppRule; 28 | import java.util.List; 29 | import java.util.TreeSet; 30 | import javax.ws.rs.client.Client; 31 | import javax.ws.rs.client.Entity; 32 | import javax.ws.rs.client.Invocation; 33 | import javax.ws.rs.core.GenericType; 34 | import javax.ws.rs.core.Response; 35 | import org.junit.AfterClass; 36 | import org.junit.BeforeClass; 37 | import org.junit.ClassRule; 38 | import org.junit.Ignore; 39 | import org.junit.Test; 40 | 41 | public class NotificationsIT { 42 | 43 | private static final String TEST_USER = "test"; 44 | 45 | @ClassRule 46 | public static final DropwizardAppRule RULE = 47 | new DropwizardAppRule( 48 | NotificationApplication.class, ResourceHelpers.resourceFilePath("notification-test.yml")); 49 | 50 | private static Client client; 51 | 52 | @BeforeClass 53 | public static void setUp() throws Exception { 54 | client = new JerseyClientBuilder(RULE.getEnvironment()).build("test client"); 55 | } 56 | 57 | @AfterClass 58 | public static void tearDown() throws Exception { 59 | client.close(); 60 | } 61 | 62 | @Test 63 | public void testCreateNotification() throws Exception { 64 | final Notification notification = createNotification(); 65 | 66 | final Response response = client.target(getUrl()).request().post(Entity.json(notification)); 67 | 68 | final Notification actual = response.readEntity(Notification.class); 69 | 70 | assertThat(response.getStatus()).isEqualTo(201); 71 | assertThat(response.getLocation().getPath()).isEqualTo("/v1/notifications/" + TEST_USER); 72 | assertThat(actual.getCategory()).isEqualTo(notification.getCategory()); 73 | assertThat(actual.getMessage()).isEqualTo(notification.getMessage()); 74 | assertThat(actual.getId().isPresent()).isTrue(); 75 | assertThat(actual.getCreatedAt()).isNotNull(); 76 | } 77 | 78 | @Test 79 | @Ignore 80 | public void testPagination() throws Exception { 81 | testDeleteAllNotifications(); 82 | 83 | final TreeSet expected = Sets.newTreeSet(); 84 | for (int i = 0; i < 100; i++) { 85 | final Notification response = 86 | client 87 | .target(getUrl()) 88 | .request() 89 | .post(Entity.json(createNotification()), Notification.class); 90 | expected.add(Notification.builder(response).withUnseen(true).build()); 91 | } 92 | 93 | int requests = 0; 94 | String nextRange = null; 95 | final ImmutableList.Builder results = ImmutableList.builder(); 96 | boolean paginate = true; 97 | while (paginate) { 98 | final Invocation.Builder builder = client.target(getUrl()).request(); 99 | if (nextRange != null) { 100 | builder.header("Range", nextRange); 101 | } 102 | 103 | final Response response = builder.get(); 104 | nextRange = response.getHeaderString("Next-Range"); 105 | if (nextRange == null) { 106 | paginate = false; 107 | } 108 | 109 | if (response.getStatus() == 200 || response.getStatus() == 206) { 110 | final List list = 111 | response.readEntity(new GenericType>() {}); 112 | assertThat(list.size()).isEqualTo(20); 113 | results.addAll(list); 114 | } 115 | 116 | requests++; 117 | } 118 | 119 | final List actual = results.build(); 120 | assertThat(actual).containsExactlyElementsOf(expected); 121 | assertThat(requests).isEqualTo(5); 122 | 123 | testDeleteAllNotifications(); 124 | } 125 | 126 | @Test 127 | public void testFetchNotifications() { 128 | final Response response = client.target(getUrl()).request().get(); 129 | 130 | if (response.getStatus() != 404) { 131 | assertThat(response.getStatus()).isEqualTo(200); 132 | final List actual = 133 | response.readEntity(new GenericType>() {}); 134 | assertThat(actual.size()).isGreaterThan(0); 135 | final Notification first = actual.get(0); 136 | assertThat(first.getId().isPresent()).isTrue(); 137 | assertThat(first.getCreatedAt()).isNotNull(); 138 | } 139 | } 140 | 141 | @Test 142 | public void testDeleteAllNotifications() { 143 | final Response response = client.target(getUrl()).request().delete(); 144 | assertThat(response.getStatus()).isEqualTo(204); 145 | } 146 | 147 | private static String getUrl() { 148 | return String.format( 149 | "http://127.0.0.1:%d/api/v1/notifications/%s", RULE.getLocalPort(), TEST_USER); 150 | } 151 | 152 | private static Notification createNotification() { 153 | return Notification.builder("test-category", "this is only a test").build(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/managed/CursorStoreManagerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.managed; 17 | 18 | import static org.mockito.Mockito.mock; 19 | import static org.mockito.Mockito.verify; 20 | 21 | import com.smoketurner.notification.application.store.CursorStore; 22 | import org.junit.Test; 23 | 24 | public class CursorStoreManagerTest { 25 | 26 | private final CursorStore store = mock(CursorStore.class); 27 | 28 | @Test 29 | public void testStart() throws Exception { 30 | final CursorStoreManager manager = new CursorStoreManager(store); 31 | manager.start(); 32 | verify(store).initialize(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/managed/NotificationStoreManagerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.managed; 17 | 18 | import static org.mockito.Mockito.mock; 19 | import static org.mockito.Mockito.verify; 20 | 21 | import com.smoketurner.notification.application.store.NotificationStore; 22 | import org.junit.Test; 23 | 24 | public class NotificationStoreManagerTest { 25 | 26 | private final NotificationStore store = mock(NotificationStore.class); 27 | 28 | @Test 29 | public void testStart() throws Exception { 30 | final NotificationStoreManager manager = new NotificationStoreManager(store); 31 | manager.start(); 32 | verify(store).initialize(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/resources/PingResourceTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.resources; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import io.dropwizard.testing.junit.ResourceTestRule; 21 | import javax.ws.rs.core.Response; 22 | import org.junit.ClassRule; 23 | import org.junit.Test; 24 | 25 | public class PingResourceTest { 26 | 27 | @ClassRule 28 | public static final ResourceTestRule resources = 29 | ResourceTestRule.builder().addResource(new PingResource()).build(); 30 | 31 | @Test 32 | public void testGetPing() throws Exception { 33 | final Response response = resources.client().target("/ping").request().get(); 34 | final String actual = response.readEntity(String.class); 35 | 36 | assertThat(response.getStatus()).isEqualTo(200); 37 | assertThat(actual).isEqualTo("pong"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/resources/VersionResourceTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.resources; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import io.dropwizard.testing.junit.ResourceTestRule; 21 | import javax.ws.rs.core.Response; 22 | import org.junit.ClassRule; 23 | import org.junit.Test; 24 | 25 | public class VersionResourceTest { 26 | 27 | private static final String VERSION = "1.0.0-TEST"; 28 | 29 | @ClassRule 30 | public static final ResourceTestRule resources = 31 | ResourceTestRule.builder().addResource(new VersionResource(VERSION)).build(); 32 | 33 | @Test 34 | public void testVersion() throws Exception { 35 | final Response response = resources.client().target("/version").request().get(); 36 | final String actual = response.readEntity(String.class); 37 | 38 | assertThat(response.getStatus()).isEqualTo(200); 39 | assertThat(actual).isEqualTo(VERSION); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/riak/CursorObjectTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import static io.dropwizard.testing.FixtureHelpers.fixture; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | 21 | import com.fasterxml.jackson.databind.ObjectMapper; 22 | import io.dropwizard.jackson.Jackson; 23 | import java.util.TreeSet; 24 | import org.junit.Before; 25 | import org.junit.Test; 26 | 27 | public class CursorObjectTest { 28 | 29 | private final ObjectMapper MAPPER = Jackson.newObjectMapper(); 30 | private CursorObject cursor; 31 | 32 | @Before 33 | public void setUp() { 34 | cursor = new CursorObject("test-notifications", "3"); 35 | } 36 | 37 | @Test 38 | public void serializesToJSON() throws Exception { 39 | final String actual = MAPPER.writeValueAsString(cursor); 40 | final String expected = 41 | MAPPER.writeValueAsString( 42 | MAPPER.readValue(fixture("fixtures/cursor.json"), CursorObject.class)); 43 | assertThat(actual).isEqualTo(expected); 44 | } 45 | 46 | @Test 47 | public void deserializesFromJSON() throws Exception { 48 | final CursorObject actual = 49 | MAPPER.readValue(fixture("fixtures/cursor.json"), CursorObject.class); 50 | assertThat(actual).isEqualTo(cursor); 51 | } 52 | 53 | @Test 54 | public void testGetKey() { 55 | assertThat(cursor.getKey()).isEqualTo("test-notifications"); 56 | } 57 | 58 | @Test 59 | public void testGetValue() { 60 | assertThat(cursor.getValue()).isEqualTo("3"); 61 | } 62 | 63 | @Test 64 | public void testToString() { 65 | assertThat(cursor.toString()).isEqualTo("CursorObject{key=test-notifications, value=3}"); 66 | } 67 | 68 | @Test 69 | public void testCursorSorting() { 70 | final CursorObject c2 = new CursorObject("test-notifications", "1"); 71 | final CursorObject c3 = new CursorObject("test-notifications", "2"); 72 | final TreeSet cursors = new TreeSet<>(); 73 | cursors.add(cursor); 74 | cursors.add(c2); 75 | cursors.add(c3); 76 | assertThat(cursors).containsExactly(cursor, c3, c2); 77 | } 78 | 79 | @Test 80 | public void testNaturalOrdering() { 81 | final CursorObject c1 = new CursorObject("test-notifications", "1"); 82 | final CursorObject c2 = new CursorObject("test-notifications", "2"); 83 | final CursorObject c3 = new CursorObject("test-other", "2"); 84 | assertThat(c1.equals(c2)).isEqualTo(c1.compareTo(c2) == 0); 85 | assertThat(c2.equals(c3)).isEqualTo(c2.compareTo(c3) == 0); 86 | assertThat(c1.equals(c3)).isEqualTo(c1.compareTo(c3) == 0); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/riak/CursorResolverTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import java.util.Arrays; 21 | import java.util.Collections; 22 | import java.util.List; 23 | import org.junit.Test; 24 | 25 | public class CursorResolverTest { 26 | 27 | private final CursorResolver resolver = new CursorResolver(); 28 | 29 | @Test 30 | public void testNoSiblings() throws Exception { 31 | final List siblings = Collections.emptyList(); 32 | final CursorObject actual = resolver.resolve(siblings); 33 | assertThat(actual).isNull(); 34 | } 35 | 36 | @Test 37 | public void testSingleSibling() throws Exception { 38 | final CursorObject list = new CursorObject("test", "1"); 39 | final List siblings = Collections.singletonList(list); 40 | final CursorObject actual = resolver.resolve(siblings); 41 | assertThat(actual).isEqualTo(list); 42 | } 43 | 44 | @Test 45 | @SuppressWarnings("NullAway") 46 | public void testMultipleSibling() throws Exception { 47 | final CursorObject cursor1 = new CursorObject("test", "1"); 48 | final CursorObject cursor2 = new CursorObject("test", "2"); 49 | final CursorObject cursor3 = new CursorObject("test", "3"); 50 | final CursorObject cursor4 = new CursorObject("test", "4"); 51 | final CursorObject cursor5 = new CursorObject("test", "5"); 52 | final CursorObject cursor6 = new CursorObject("test", "6"); 53 | 54 | final List siblings = 55 | Arrays.asList(cursor1, cursor2, cursor3, cursor4, cursor5, cursor6); 56 | 57 | final CursorObject actual = resolver.resolve(siblings); 58 | assertThat(actual).isEqualTo(cursor6); 59 | assertThat(actual.getValue()).isEqualTo("6"); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/riak/CursorUpdateTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import org.junit.Test; 21 | 22 | public class CursorUpdateTest { 23 | 24 | @Test 25 | public void testUpdatesCursor() { 26 | final CursorUpdate update = new CursorUpdate("test-notifications", "12345"); 27 | 28 | final CursorObject original = new CursorObject("test-notifications", "1"); 29 | 30 | final CursorObject expected = new CursorObject("test-notifications", "12345"); 31 | 32 | final CursorObject actual = update.apply(original); 33 | assertThat(actual).isEqualTo(expected); 34 | } 35 | 36 | @Test 37 | public void testNoOriginal() { 38 | final CursorUpdate update = new CursorUpdate("test-notifications", "12345"); 39 | 40 | final CursorObject expected = new CursorObject("test-notifications", "12345"); 41 | 42 | final CursorObject actual = update.apply(null); 43 | assertThat(actual).isEqualTo(expected); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/riak/NotificationListAdditionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import com.smoketurner.notification.api.Notification; 21 | import org.junit.Test; 22 | 23 | public class NotificationListAdditionTest { 24 | 25 | @Test 26 | public void testAddsToNotification() { 27 | final Notification notification = Notification.create("1"); 28 | 29 | final NotificationListAddition update = new NotificationListAddition(notification); 30 | 31 | final NotificationListObject original = new NotificationListObject(); 32 | 33 | final NotificationListObject expected = new NotificationListObject(); 34 | expected.addNotification(notification); 35 | 36 | final NotificationListObject actual = update.apply(original); 37 | 38 | assertThat(actual).isEqualTo(expected); 39 | } 40 | 41 | @Test 42 | public void testNoOriginal() { 43 | final Notification notification = Notification.create("1"); 44 | 45 | final NotificationListAddition update = new NotificationListAddition(notification); 46 | 47 | final NotificationListObject expected = new NotificationListObject(); 48 | expected.addNotification(notification); 49 | 50 | final NotificationListObject actual = update.apply(null); 51 | 52 | assertThat(actual).isEqualTo(expected); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/riak/NotificationListConverterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import com.basho.riak.client.api.convert.ConversionException; 21 | import com.basho.riak.client.core.util.BinaryValue; 22 | import com.smoketurner.notification.api.Notification; 23 | import com.smoketurner.notification.application.protos.NotificationProtos.NotificationListPB; 24 | import com.smoketurner.notification.application.protos.NotificationProtos.NotificationPB; 25 | import java.time.ZonedDateTime; 26 | import org.junit.Test; 27 | 28 | public class NotificationListConverterTest { 29 | 30 | private final NotificationListConverter converter = new NotificationListConverter(); 31 | 32 | @Test(expected = ConversionException.class) 33 | public void testToDomainInvalidContentType() throws Exception { 34 | converter.toDomain(BinaryValue.create("test"), "text/plain"); 35 | } 36 | 37 | @Test(expected = ConversionException.class) 38 | public void testToDomainInvalidData() throws Exception { 39 | converter.toDomain(BinaryValue.create("test"), "application/x-protobuf"); 40 | } 41 | 42 | @Test 43 | public void testToDomain() throws Exception { 44 | final ZonedDateTime now = ZonedDateTime.parse("2015-08-14T17:52:43Z"); 45 | 46 | final Notification n1 = 47 | Notification.builder("test-category", "this is a test") 48 | .withId("1") 49 | .withCreatedAt(now) 50 | .build(); 51 | final NotificationListObject expected = new NotificationListObject(); 52 | expected.addNotification(n1); 53 | 54 | final NotificationPB pb = 55 | NotificationPB.newBuilder() 56 | .setId("1") 57 | .setCategory("test-category") 58 | .setMessage("this is a test") 59 | .setCreatedAt(now.toInstant().toEpochMilli()) 60 | .build(); 61 | 62 | final NotificationListPB list = NotificationListPB.newBuilder().addNotification(pb).build(); 63 | 64 | final NotificationListObject actual = 65 | converter.toDomain(BinaryValue.create(list.toByteArray()), "application/x-protobuf"); 66 | assertThat(actual).isEqualTo(expected); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/riak/NotificationListDeletionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import com.google.common.collect.ImmutableList; 21 | import org.junit.Test; 22 | 23 | public class NotificationListDeletionTest { 24 | 25 | @Test 26 | public void testDeletesFromNotification() { 27 | final ImmutableList ids = ImmutableList.of("1", "2", "3"); 28 | final NotificationListDeletion update = new NotificationListDeletion(ids); 29 | 30 | final NotificationListObject original = new NotificationListObject(); 31 | 32 | final NotificationListObject expected = new NotificationListObject(); 33 | expected.deleteNotifications(ids); 34 | 35 | final NotificationListObject actual = update.apply(original); 36 | 37 | assertThat(actual).isEqualTo(expected); 38 | } 39 | 40 | @Test 41 | public void testNoOriginal() { 42 | final ImmutableList ids = ImmutableList.of("1", "2", "3"); 43 | final NotificationListDeletion update = new NotificationListDeletion(ids); 44 | 45 | final NotificationListObject expected = new NotificationListObject(); 46 | expected.deleteNotifications(ids); 47 | 48 | final NotificationListObject actual = update.apply(null); 49 | 50 | assertThat(actual).isEqualTo(expected); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/riak/NotificationListObjectTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.riak; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import com.google.common.collect.ImmutableList; 21 | import com.smoketurner.notification.api.Notification; 22 | import java.util.SortedSet; 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | 26 | public class NotificationListObjectTest { 27 | 28 | private NotificationListObject list; 29 | 30 | @Before 31 | public void setUp() { 32 | list = new NotificationListObject("test"); 33 | } 34 | 35 | @Test 36 | public void testMaximumNumberOfNotifications() { 37 | for (int i = 0; i <= 2000; i++) { 38 | list.addNotification(Notification.create(String.format("%04d", i))); 39 | } 40 | 41 | final SortedSet actual = list.getNotifications(); 42 | assertThat(actual).hasSize(1000); 43 | assertThat(actual.first().getId().get()).isEqualTo("2000"); 44 | assertThat(actual.last().getId().get()).isEqualTo("1001"); 45 | } 46 | 47 | @Test 48 | public void testMaximumNumberOfNotificationsCollection() { 49 | final ImmutableList.Builder builder = ImmutableList.builder(); 50 | for (int i = 0; i <= 2000; i++) { 51 | builder.add(Notification.create(String.format("%04d", i))); 52 | } 53 | 54 | list.addNotifications(builder.build()); 55 | 56 | final SortedSet actual = list.getNotifications(); 57 | assertThat(actual).hasSize(1000); 58 | assertThat(actual.first().getId().get()).isEqualTo("2000"); 59 | assertThat(actual.last().getId().get()).isEqualTo("1001"); 60 | } 61 | 62 | @Test 63 | public void testNoDuplicateNotifications() { 64 | for (int i = 0; i < 5; i++) { 65 | list.addNotification(Notification.create("1")); 66 | } 67 | 68 | final SortedSet actual = list.getNotifications(); 69 | assertThat(actual).hasSize(1); 70 | assertThat(actual.first().getId().get()).isEqualTo("1"); 71 | } 72 | 73 | @Test 74 | @SuppressWarnings("NullAway") 75 | public void testEquals() { 76 | final NotificationListObject list = new NotificationListObject(); 77 | assertThat(list.equals(null)).isFalse(); 78 | } 79 | 80 | @Test 81 | public void testDeleteNotification() { 82 | list.deleteNotification("1"); 83 | assertThat(list.getDeletedIds()).contains("1"); 84 | } 85 | 86 | @Test 87 | public void testGetKey() { 88 | assertThat(list.getKey()).isEqualTo("test"); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /notification-application/src/test/java/com/smoketurner/notification/application/store/CursorStoreTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.application.store; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; 20 | import static org.mockito.ArgumentMatchers.any; 21 | import static org.mockito.Mockito.mock; 22 | import static org.mockito.Mockito.never; 23 | import static org.mockito.Mockito.verify; 24 | import static org.mockito.Mockito.when; 25 | 26 | import com.basho.riak.client.api.RiakClient; 27 | import com.basho.riak.client.api.commands.buckets.StoreBucketProperties; 28 | import com.basho.riak.client.api.commands.kv.DeleteValue; 29 | import com.basho.riak.client.api.commands.kv.FetchValue; 30 | import com.basho.riak.client.api.commands.kv.UpdateValue; 31 | import io.dropwizard.util.Duration; 32 | import java.util.Optional; 33 | import org.junit.Ignore; 34 | import org.junit.Test; 35 | 36 | public class CursorStoreTest { 37 | 38 | private static final String TEST_USER = "test"; 39 | private static final String CURSOR_NAME = "notifications"; 40 | private final RiakClient client = mock(RiakClient.class); 41 | private final CursorStore store = 42 | new CursorStore(client, Duration.seconds(60), Duration.seconds(5)); 43 | 44 | @Test 45 | public void testInitialize() throws Exception { 46 | store.initialize(); 47 | verify(client).execute(any(StoreBucketProperties.class)); 48 | } 49 | 50 | @Test 51 | @Ignore 52 | public void testFetch() throws Exception { 53 | final FetchValue.Response response = mock(FetchValue.Response.class); 54 | 55 | final Optional expected = Optional.of(1L); 56 | 57 | when(client.execute(any(FetchValue.class))).thenReturn(response); 58 | 59 | final Optional actual = store.fetch(TEST_USER, CURSOR_NAME); 60 | verify(client).execute(any(FetchValue.class)); 61 | assertThat(actual).isEqualTo(expected); 62 | } 63 | 64 | @Test 65 | public void testFetchEmptyUsername() throws Exception { 66 | try { 67 | store.fetch("", CURSOR_NAME); 68 | failBecauseExceptionWasNotThrown(IllegalArgumentException.class); 69 | } catch (IllegalArgumentException e) { 70 | } 71 | verify(client, never()).execute(any(FetchValue.class)); 72 | } 73 | 74 | @Test 75 | public void testFetchEmptyCursor() throws Exception { 76 | try { 77 | store.fetch("test", ""); 78 | failBecauseExceptionWasNotThrown(IllegalArgumentException.class); 79 | } catch (IllegalArgumentException e) { 80 | } 81 | verify(client, never()).execute(any(FetchValue.class)); 82 | } 83 | 84 | @Test 85 | @Ignore 86 | public void testStore() throws Exception { 87 | store.store(TEST_USER, CURSOR_NAME, "1"); 88 | verify(client).executeAsync(any(UpdateValue.class)); 89 | } 90 | 91 | @Test 92 | public void testStoreEmptyUsername() throws Exception { 93 | try { 94 | store.store("", CURSOR_NAME, "1"); 95 | failBecauseExceptionWasNotThrown(IllegalArgumentException.class); 96 | } catch (IllegalArgumentException e) { 97 | } 98 | verify(client, never()).execute(any(UpdateValue.class)); 99 | } 100 | 101 | @Test 102 | public void testStoreEmptyCursorName() throws Exception { 103 | try { 104 | store.store(TEST_USER, "", "1"); 105 | failBecauseExceptionWasNotThrown(IllegalArgumentException.class); 106 | } catch (IllegalArgumentException e) { 107 | } 108 | verify(client, never()).execute(any(UpdateValue.class)); 109 | } 110 | 111 | @Test 112 | @Ignore 113 | public void testDelete() throws Exception { 114 | store.delete(TEST_USER, CURSOR_NAME); 115 | verify(client).executeAsync(any(DeleteValue.class)); 116 | } 117 | 118 | @Test 119 | public void testDeleteEmptyUsername() throws Exception { 120 | try { 121 | store.delete("", CURSOR_NAME); 122 | failBecauseExceptionWasNotThrown(IllegalArgumentException.class); 123 | } catch (IllegalArgumentException e) { 124 | } 125 | verify(client, never()).execute(any(DeleteValue.class)); 126 | } 127 | 128 | @Test 129 | public void testDeleteEmptyCursorName() throws Exception { 130 | try { 131 | store.delete(TEST_USER, ""); 132 | failBecauseExceptionWasNotThrown(IllegalArgumentException.class); 133 | } catch (IllegalArgumentException e) { 134 | } 135 | verify(client, never()).execute(any(DeleteValue.class)); 136 | } 137 | 138 | @Test 139 | public void testGetCursorKey() { 140 | assertThat(store.getCursorKey(TEST_USER, CURSOR_NAME)).isEqualTo("test-notifications"); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /notification-application/src/test/resources/Notification.graphql: -------------------------------------------------------------------------------- 1 | scalar Map 2 | 3 | schema { 4 | query: Query 5 | mutation: Mutation 6 | } 7 | 8 | type Query { 9 | notifications(username: String!): [Notification!] 10 | rules: [RuleCategory!] 11 | } 12 | 13 | type Mutation { 14 | createNotification(username: String!, notification: NotificationInput!): Notification 15 | createRule(category: String!, rule: RuleInput!): Boolean! 16 | removeAllNotifications(username: String!): Boolean! 17 | removeAllRules: Boolean! 18 | removeNotification(username: String!, ids: [ID!]!): Boolean! 19 | removeRule(category: String!): Boolean! 20 | } 21 | 22 | type Notification { 23 | id: ID! 24 | category: String! 25 | message: String! 26 | unseen: Boolean! 27 | createdAt: String! 28 | properties: Map! 29 | notifications: [Notification!] 30 | } 31 | 32 | type RuleCategory { 33 | category: String! 34 | rule: Rule! 35 | } 36 | 37 | type Rule { 38 | maxSize: Int 39 | maxDuration: String 40 | matchOn: String 41 | } 42 | 43 | input NotificationInput { 44 | category: String! 45 | message: String! 46 | properties: Map 47 | } 48 | 49 | input RuleInput { 50 | maxSize: Int 51 | maxDuration: String 52 | matchOn: String 53 | } 54 | -------------------------------------------------------------------------------- /notification-application/src/test/resources/fixtures/cursor.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "test-notifications", 3 | "value": "3" 4 | } -------------------------------------------------------------------------------- /notification-application/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /notification-application/src/test/resources/notification-test.yml: -------------------------------------------------------------------------------- 1 | # GraphQL-specific options. 2 | graphql: 3 | 4 | enableTracing: false 5 | queryCache: maximumSize=0 6 | schemaFiles: 7 | - Notification.graphql 8 | 9 | # Riak-specific options. 10 | riak: 11 | 12 | nodes: 13 | - 127.0.0.1:8087 14 | 15 | # HTTP-specific options. 16 | server: 17 | 18 | type: simple 19 | rootPath: /api/ 20 | applicationContextPath: / 21 | connector: 22 | type: http 23 | port: 0 24 | -------------------------------------------------------------------------------- /notification-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 4.0.0 21 | 22 | 23 | com.smoketurner.notification 24 | notification-parent 25 | 2.0.1-SNAPSHOT 26 | 27 | 28 | notification-client 29 | Notification Client 30 | 31 | 32 | 33 | ${project.groupId} 34 | notification-api 35 | ${project.version} 36 | 37 | 38 | io.dropwizard 39 | dropwizard-client 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /notification-client/src/main/java/com/smoketurner/notification/client/NotificationClientBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.client; 17 | 18 | import io.dropwizard.client.JerseyClientBuilder; 19 | import io.dropwizard.setup.Environment; 20 | import java.util.Objects; 21 | import javax.ws.rs.client.Client; 22 | 23 | public class NotificationClientBuilder { 24 | private final Environment environment; 25 | 26 | /** 27 | * Constructor 28 | * 29 | * @param environment Environment 30 | */ 31 | public NotificationClientBuilder(final Environment environment) { 32 | this.environment = Objects.requireNonNull(environment, "environment == null"); 33 | } 34 | 35 | /** 36 | * Build a new {@link NotificationClient} 37 | * 38 | * @param configuration Configuration to use for the client 39 | * @return new NotificationClient 40 | */ 41 | public NotificationClient build(final NotificationClientConfiguration configuration) { 42 | final Client client = 43 | new JerseyClientBuilder(environment).using(configuration).build("notification"); 44 | return build(configuration, client); 45 | } 46 | 47 | /** 48 | * Build a new {@link NotificationClient} 49 | * 50 | * @param configuration Configuration to use for the client 51 | * @param client Jersey Client to use 52 | * @return new NotificationClient 53 | */ 54 | public NotificationClient build( 55 | final NotificationClientConfiguration configuration, final Client client) { 56 | return new NotificationClient(environment.metrics(), client, configuration.getUri()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /notification-client/src/main/java/com/smoketurner/notification/client/NotificationClientConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.client; 17 | 18 | import io.dropwizard.client.JerseyClientConfiguration; 19 | import java.net.URI; 20 | import org.hibernate.validator.constraints.NotEmpty; 21 | 22 | public class NotificationClientConfiguration extends JerseyClientConfiguration { 23 | 24 | @NotEmpty private String uri = "http://127.0.0.1:8080/api"; 25 | 26 | public URI getUri() { 27 | return URI.create(uri); 28 | } 29 | 30 | public void setUri(final String uri) { 31 | this.uri = uri; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /notification-client/src/test/java/com/smoketurner/notification/client/NotificationClientTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.client; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import com.google.common.collect.ImmutableList; 21 | import com.smoketurner.notification.api.Notification; 22 | import io.dropwizard.client.JerseyClientBuilder; 23 | import io.dropwizard.testing.junit.DropwizardClientRule; 24 | import java.util.List; 25 | import java.util.Optional; 26 | import java.util.SortedSet; 27 | import javax.ws.rs.Consumes; 28 | import javax.ws.rs.DELETE; 29 | import javax.ws.rs.GET; 30 | import javax.ws.rs.POST; 31 | import javax.ws.rs.Path; 32 | import javax.ws.rs.PathParam; 33 | import javax.ws.rs.Produces; 34 | import javax.ws.rs.QueryParam; 35 | import javax.ws.rs.client.Client; 36 | import javax.ws.rs.core.MediaType; 37 | import javax.ws.rs.core.Response; 38 | import org.junit.AfterClass; 39 | import org.junit.BeforeClass; 40 | import org.junit.ClassRule; 41 | import org.junit.Test; 42 | 43 | public class NotificationClientTest { 44 | 45 | @Path("/v1/notifications/{username}") 46 | public static class NotificationResource { 47 | @GET 48 | @Produces(MediaType.APPLICATION_JSON) 49 | public List fetch(@PathParam("username") String username) { 50 | return ImmutableList.of(Notification.create("1")); 51 | } 52 | 53 | @POST 54 | @Consumes(MediaType.APPLICATION_JSON) 55 | @Produces(MediaType.APPLICATION_JSON) 56 | public Notification store(@PathParam("username") String username, Notification notification) { 57 | return notification; 58 | } 59 | 60 | @DELETE 61 | public Response delete(@PathParam("username") String username, @QueryParam("ids") String ids) { 62 | return Response.noContent().build(); 63 | } 64 | } 65 | 66 | @Path("/ping") 67 | public static class PingResource { 68 | @GET 69 | public String ping() { 70 | return "pong"; 71 | } 72 | } 73 | 74 | @Path("/version") 75 | public static class VersionResource { 76 | @GET 77 | public String version() { 78 | return "1.0.0"; 79 | } 80 | } 81 | 82 | @ClassRule 83 | public static final DropwizardClientRule resources = 84 | new DropwizardClientRule( 85 | new NotificationResource(), new PingResource(), new VersionResource()); 86 | 87 | private static NotificationClient client; 88 | 89 | @BeforeClass 90 | public static void setUp() { 91 | final Client jerseyClient = new JerseyClientBuilder(resources.getEnvironment()).build("test"); 92 | client = 93 | new NotificationClient( 94 | resources.getEnvironment().metrics(), jerseyClient, resources.baseUri()); 95 | } 96 | 97 | @AfterClass 98 | public static void tearDown() throws Exception { 99 | client.close(); 100 | } 101 | 102 | @Test 103 | public void testFetch() throws Exception { 104 | final Optional> actual = client.fetch("test"); 105 | assertThat(actual.isPresent()).isTrue(); 106 | final SortedSet notifications = actual.get(); 107 | assertThat(notifications.size()).isEqualTo(1); 108 | assertThat(notifications.first().getId().isPresent()).isTrue(); 109 | } 110 | 111 | @Test(expected = IllegalArgumentException.class) 112 | public void testFetchEmptyUsername() throws Exception { 113 | client.fetch(""); 114 | } 115 | 116 | @Test 117 | public void testStore() throws Exception { 118 | final Notification expected = Notification.create("1"); 119 | final Optional actual = client.store("test", expected); 120 | assertThat(actual.isPresent()).isTrue(); 121 | assertThat(actual.get()).isEqualTo(expected); 122 | } 123 | 124 | @Test(expected = IllegalArgumentException.class) 125 | public void testStoreEmptyUsername() throws Exception { 126 | final Notification expected = Notification.create("1"); 127 | client.store("", expected); 128 | } 129 | 130 | @Test 131 | public void testDeleteAll() throws Exception { 132 | client.delete("test"); 133 | } 134 | 135 | @Test(expected = IllegalArgumentException.class) 136 | public void testDeleteAllEmptyUsername() throws Exception { 137 | client.delete(""); 138 | } 139 | 140 | @Test 141 | public void testDelete() throws Exception { 142 | client.delete("test", ImmutableList.of("1", "2")); 143 | } 144 | 145 | @Test(expected = IllegalArgumentException.class) 146 | public void testDeleteEmptyUsername() throws Exception { 147 | client.delete("", ImmutableList.of("1")); 148 | } 149 | 150 | @Test(expected = IllegalArgumentException.class) 151 | public void testDeleteEmptyIds() throws Exception { 152 | client.delete("test", ImmutableList.of()); 153 | } 154 | 155 | @Test 156 | public void testPing() throws Exception { 157 | assertThat(client.ping()).isTrue(); 158 | } 159 | 160 | @Test 161 | public void testVersion() throws Exception { 162 | assertThat(client.version()).isEqualTo("1.0.0"); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /notification-client/src/test/java/com/smoketurner/notification/client/integration/NotificationGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Smoke Turner, LLC (github@smoketurner.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smoketurner.notification.client.integration; 17 | 18 | import com.codahale.metrics.MetricRegistry; 19 | import com.google.common.collect.ImmutableMap; 20 | import com.smoketurner.notification.api.Notification; 21 | import com.smoketurner.notification.client.NotificationClient; 22 | import io.dropwizard.jackson.Jackson; 23 | import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; 24 | import java.net.URI; 25 | import java.util.Map; 26 | import java.util.Random; 27 | import java.util.concurrent.CountDownLatch; 28 | import java.util.concurrent.ExecutorService; 29 | import java.util.concurrent.Executors; 30 | import javax.ws.rs.client.Client; 31 | import javax.ws.rs.core.UriBuilder; 32 | import org.glassfish.jersey.client.JerseyClientBuilder; 33 | 34 | public class NotificationGenerator { 35 | 36 | private static final String USERNAME = "test"; 37 | private static final int MAX_NOTIFICATIONS = 3000; 38 | private static final int NUM_THREADS = 3; 39 | private static final Random RANDOM = new Random(); 40 | private static final Map USERS = 41 | ImmutableMap.of(0, "Blade", 1, "Spiderman", 2, "Batman", 3, "Robin", 4, "Colossus"); 42 | 43 | public static void main(String[] args) throws Exception { 44 | final MetricRegistry registry = new MetricRegistry(); 45 | final URI uri = UriBuilder.fromUri("http://localhost:8080").build(); 46 | final Client jerseyClient = 47 | new JerseyClientBuilder() 48 | .register(new JacksonMessageBodyProvider(Jackson.newObjectMapper())) 49 | .build(); 50 | final NotificationClient client = new NotificationClient(registry, jerseyClient, uri); 51 | 52 | final CountDownLatch latch = new CountDownLatch(NUM_THREADS); 53 | 54 | final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS); 55 | 56 | // new-follower 57 | executor.execute( 58 | () -> { 59 | final int count = MAX_NOTIFICATIONS / NUM_THREADS; 60 | System.out.println("Starting to send " + count + " new-followers"); 61 | 62 | int userId; 63 | String message; 64 | Notification notification; 65 | for (int i = 0; i < count; i++) { 66 | userId = RANDOM.nextInt(USERS.size()); 67 | message = String.format("%s is now following you", USERS.get(userId)); 68 | 69 | notification = 70 | Notification.builder("new-follower", message) 71 | .withProperties(ImmutableMap.of("follower_id", String.valueOf(userId))) 72 | .build(); 73 | 74 | client.store(USERNAME, notification); 75 | } 76 | System.out.println("Finished creating " + count + " new-followers"); 77 | latch.countDown(); 78 | }); 79 | 80 | // like 81 | executor.execute( 82 | () -> { 83 | final int count = MAX_NOTIFICATIONS / NUM_THREADS; 84 | System.out.println("Starting to send " + count + " likes"); 85 | 86 | int userId; 87 | String message; 88 | int messageId; 89 | Notification notification; 90 | for (int i = 0; i < count; i++) { 91 | userId = RANDOM.nextInt(USERS.size()); 92 | message = String.format("%s liked your post", USERS.get(userId)); 93 | messageId = RANDOM.nextInt(5); 94 | 95 | notification = 96 | Notification.builder("like", message) 97 | .withProperties( 98 | ImmutableMap.of( 99 | "message_id", 100 | String.valueOf(messageId), 101 | "liker_id", 102 | String.valueOf(userId))) 103 | .build(); 104 | 105 | client.store(USERNAME, notification); 106 | } 107 | System.out.println("Finished creating " + count + " likes"); 108 | latch.countDown(); 109 | }); 110 | 111 | // mention 112 | executor.execute( 113 | () -> { 114 | final int count = MAX_NOTIFICATIONS / NUM_THREADS; 115 | System.out.println("Starting to send " + count + " mentions"); 116 | 117 | int userId; 118 | String message; 119 | int messageId; 120 | Notification notification; 121 | for (int i = 0; i < count; i++) { 122 | userId = RANDOM.nextInt(USERS.size()); 123 | message = String.format("%s mentioned you in a post", USERS.get(userId)); 124 | messageId = RANDOM.nextInt(5); 125 | 126 | notification = 127 | Notification.builder("mention", message) 128 | .withProperties( 129 | ImmutableMap.of( 130 | "message_id", 131 | String.valueOf(messageId), 132 | "user_id", 133 | String.valueOf(userId))) 134 | .build(); 135 | 136 | client.store(USERNAME, notification); 137 | } 138 | System.out.println("Finished creating " + count + " mentions"); 139 | latch.countDown(); 140 | }); 141 | 142 | latch.await(); 143 | client.close(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /notification-client/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 4.0.0 21 | 22 | com.smoketurner.dropwizard 23 | dropwizard-pom 24 | 1.3.10-1 25 | 26 | 27 | com.smoketurner.notification 28 | notification-parent 29 | 2.0.1-SNAPSHOT 30 | pom 31 | 32 | notification 33 | Notification 34 | https://github.com/smoketurner/notification 35 | 36 | 37 | false 38 | 39 | 40 | 41 | notification-api 42 | notification-application 43 | notification-client 44 | 45 | 46 | 47 | scm:git:git://github.com/smoketurner/notification.git 48 | scm:git:git@github.com:smoketurner/notification.git 49 | https://github.com/smoketurner/notification 50 | HEAD 51 | 52 | 53 | 54 | 55 | javax.xml.bind 56 | jaxb-api 57 | 2.3.1 58 | runtime 59 | 60 | 61 | javax.activation 62 | javax.activation-api 63 | 1.2.0 64 | runtime 65 | 66 | 67 | 68 | 69 | 70 | 71 | kr.motd.maven 72 | os-maven-plugin 73 | 1.6.2 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /riak_schemas/maps.dt: -------------------------------------------------------------------------------- 1 | map 2 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VERSION=`xmllint --xpath "//*[local-name()='project']/*[local-name()='version']/text()" pom.xml` 4 | 5 | docker run \ 6 | --name notification \ 7 | --rm \ 8 | -e PORT=8080 \ 9 | -p 8080:8080 \ 10 | smoketurner/notification:${VERSION} 11 | -------------------------------------------------------------------------------- /spotbugs.xml: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /start_riak.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker run \ 4 | --rm \ 5 | -p 8087:8087 \ 6 | -v $(pwd)/riak_schemas:/etc/riak/schemas \ 7 | basho/riak-kv:ubuntu-2.2.3 8 | --------------------------------------------------------------------------------