├── scripts ├── saveAccount.sh ├── saveCustomer.sh ├── saveTransaction.sh ├── testPipeline.sh ├── getReturns.sh ├── getByPhone.sh ├── getTransaction.sh ├── getTransactionTags.sh ├── getByStateCity.sh ├── getTaggedAccountTransactions.sh ├── getByEmail.sh ├── getTransactionStatus.sh ├── getByZipLastname.sh ├── getByNamePhone.sh ├── getByCustID.sh ├── deleteCustomers.sh ├── getByMerchant.sh ├── deleteCustomerEmail.sh ├── getByMerchantCategory.sh ├── getByAccount.sh ├── updateTransactionStatus.sh ├── addTag.sh ├── addTag2.sh ├── getByCreditCard.sh ├── customer.json ├── customer1.json ├── customer3.json ├── customer2.json ├── customer4.json ├── customer5.json ├── customer6.json ├── generateData.sh ├── startAppServers.sh ├── putCustomer.sh └── generateLots.sh ├── src ├── .DS_Store └── main │ ├── java │ └── com │ │ └── jphaugla │ │ ├── .DS_Store │ │ ├── domain │ │ ├── Email.java │ │ ├── Phone.java │ │ ├── TransactionReturn.java │ │ ├── Merchant.java │ │ ├── Account.java │ │ ├── Transaction.java │ │ └── Customer.java │ │ ├── DemoApplication.java │ │ ├── boot │ │ ├── EmailIndex.java │ │ ├── PhoneIndex.java │ │ ├── TransactionReturnIndex.java │ │ ├── MerchantIndex.java │ │ ├── CustomerIndex.java │ │ ├── AccountIndex.java │ │ └── TransactionIndex.java │ │ ├── repository │ │ ├── PhoneRepository.java │ │ ├── CustomerRepository.java │ │ ├── MerchantRepository.java │ │ ├── AccountRepository.java │ │ ├── TransactionRepository.java │ │ ├── TransactionReturnRepository.java │ │ └── EmailRepository.java │ │ ├── config │ │ └── RedisConfig.java │ │ ├── service │ │ ├── AsyncService.java │ │ └── BankService.java │ │ ├── controller │ │ └── BankingController.java │ │ └── data │ │ └── BankGenerator.java │ └── resources │ ├── ssl │ ├── importkey.sh │ ├── generatepems.sh │ ├── generatekeystore.sh │ └── generatetrust.sh │ └── application.properties ├── digitalbanking.png ├── images ├── Tables.png ├── Benchmark.png ├── DigitalBanking.png └── Springindexes.png ├── app_preview_image.png ├── runJob.sh ├── docker-compose.yml ├── marketplace.json ├── pom.xml └── README.md /scripts/saveAccount.sh: -------------------------------------------------------------------------------- 1 | # save a sample account 2 | curl http://localhost:8080/save_account 3 | -------------------------------------------------------------------------------- /scripts/saveCustomer.sh: -------------------------------------------------------------------------------- 1 | # save a sample customer 2 | curl http://localhost:8080/save_customer 3 | -------------------------------------------------------------------------------- /scripts/saveTransaction.sh: -------------------------------------------------------------------------------- 1 | # save a sample transaction 2 | curl http://localhost:8080/save_transaction 3 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/src/.DS_Store -------------------------------------------------------------------------------- /scripts/testPipeline.sh: -------------------------------------------------------------------------------- 1 | # test pipeline 2 | curl 'http://localhost:8080/testPipeline?noOfRecords=500' 3 | 4 | -------------------------------------------------------------------------------- /digitalbanking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/digitalbanking.png -------------------------------------------------------------------------------- /images/Tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/images/Tables.png -------------------------------------------------------------------------------- /images/Benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/images/Benchmark.png -------------------------------------------------------------------------------- /app_preview_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/app_preview_image.png -------------------------------------------------------------------------------- /images/DigitalBanking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/images/DigitalBanking.png -------------------------------------------------------------------------------- /images/Springindexes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/images/Springindexes.png -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/src/main/java/com/jphaugla/.DS_Store -------------------------------------------------------------------------------- /src/main/resources/ssl/importkey.sh: -------------------------------------------------------------------------------- 1 | keytool -import \ 2 | -keystore ./client-truststore.p12 \ 3 | -file ./proxy_cert.pem \ 4 | -alias redis-cluster-crt 5 | -------------------------------------------------------------------------------- /scripts/getReturns.sh: -------------------------------------------------------------------------------- 1 | # get all returned transactions 2 | # 3 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/returned_transactions' 4 | -------------------------------------------------------------------------------- /scripts/getByPhone.sh: -------------------------------------------------------------------------------- 1 | # get customers by phone only 2 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/customerByPhone/?phoneString=1000085Jh' 3 | -------------------------------------------------------------------------------- /scripts/getTransaction.sh: -------------------------------------------------------------------------------- 1 | # get one transaction by ID 2 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/getTransaction/?transactionID=45115J' 3 | -------------------------------------------------------------------------------- /scripts/getTransactionTags.sh: -------------------------------------------------------------------------------- 1 | # get all tags on an account 2 | # 3 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/getTags/?transactionID=4484J' 4 | -------------------------------------------------------------------------------- /scripts/getByStateCity.sh: -------------------------------------------------------------------------------- 1 | # get customers by city and state 2 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/customerByStateCity/?state=AK&city=Rockford' 3 | -------------------------------------------------------------------------------- /src/main/resources/ssl/generatepems.sh: -------------------------------------------------------------------------------- 1 | openssl req \ 2 | -nodes \ 3 | -newkey rsa:2048 \ 4 | -keyout client_key_app_001.pem \ 5 | -x509 \ 6 | -days 36500 \ 7 | -out client_cert_app_001.pem 8 | -------------------------------------------------------------------------------- /src/main/resources/ssl/generatekeystore.sh: -------------------------------------------------------------------------------- 1 | openssl pkcs12 -export \ 2 | -in ./client_cert_app_001.pem \ 3 | -inkey ./client_key_app_001.pem \ 4 | -out client-keystore.p12 \ 5 | -name "APP_01_P12" 6 | -------------------------------------------------------------------------------- /scripts/getTaggedAccountTransactions.sh: -------------------------------------------------------------------------------- 1 | # get all tags on an account 2 | # 3 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/getTaggedTransactions/?accountNo=Acct936J&tag=Travel' 4 | -------------------------------------------------------------------------------- /scripts/getByEmail.sh: -------------------------------------------------------------------------------- 1 | # retrieve customer record using email address 2 | # get by email only 3 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/customerByEmail/?email=1000013J@gmail.com' 4 | -------------------------------------------------------------------------------- /scripts/getTransactionStatus.sh: -------------------------------------------------------------------------------- 1 | # see count of transactions by account status of PENDING, AUTHORIZED, SETTLED 2 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/transactionStatusReport' 3 | -------------------------------------------------------------------------------- /scripts/getByZipLastname.sh: -------------------------------------------------------------------------------- 1 | # get by zipcode and lastname. Lastname is a generated integer 2 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/customerByZipcodeLastname/?zipcode=55435&lastname=00097' 3 | -------------------------------------------------------------------------------- /scripts/getByNamePhone.sh: -------------------------------------------------------------------------------- 1 | # get by phone and full name. Notice the ascii string for the space 2 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/customerByPhone/?phoneString=1000088Jw&full_name=Igor%20Golov%2000088' 3 | -------------------------------------------------------------------------------- /scripts/getByCustID.sh: -------------------------------------------------------------------------------- 1 | # retrieve transations for customer 2 | # get using a customer id. Use redisinsight's search to find a good custid 3 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/customer/?customerId=cust0001' 4 | -------------------------------------------------------------------------------- /scripts/deleteCustomers.sh: -------------------------------------------------------------------------------- 1 | # retrieve transations for customer 2 | # get using a customer id. Use redisinsight's search to find a good custid 3 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/deleteCustomer/?customerString=cust0*' 4 | -------------------------------------------------------------------------------- /scripts/getByMerchant.sh: -------------------------------------------------------------------------------- 1 | # find all transactions for an account from one merchant in date range 2 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/merchantTransactions/?merchant=BestBuy&account=Acct1380J&from=2021-06-27&to=2021-09-20' 3 | -------------------------------------------------------------------------------- /scripts/deleteCustomerEmail.sh: -------------------------------------------------------------------------------- 1 | # retrieve transations for customer 2 | # get using a customer id. Use redisinsight's search to find a good custid 3 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/deleteCustomerEmail/?customerId=cust006' 4 | -------------------------------------------------------------------------------- /scripts/getByMerchantCategory.sh: -------------------------------------------------------------------------------- 1 | # find all transactions for an account from merchant category for date range 2 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/merchantCategoryTransactions/?merchantCategory=5732&account=Acct1067J&from=2021-07-27&to=2021-09-30' 3 | -------------------------------------------------------------------------------- /src/main/resources/ssl/generatetrust.sh: -------------------------------------------------------------------------------- 1 | keytool -genkey \ 2 | -dname "cn=CLIENT_APP_01" \ 3 | -alias truststorekey \ 4 | -keyalg RSA \ 5 | -keystore ./client-truststore.p12 \ 6 | -keypass ${KEYSTORE_PASSWORD} \ 7 | -storepass ${TRUSTSTORE_PASSWORD} \ 8 | -storetype pkcs12 9 | -------------------------------------------------------------------------------- /scripts/getByAccount.sh: -------------------------------------------------------------------------------- 1 | # easiest to look up an account using redinsight and edit this script to find an existing account 2 | # date can be looked up and put in range 3 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/accountTransactions/?accountNo=Acct1514J&from=2021-07-21&to=2021-09-21' 4 | -------------------------------------------------------------------------------- /runJob.sh: -------------------------------------------------------------------------------- 1 | java -Djavax.net.debug=ssl -Djavax.net.ssl.keyStore=src/main/resources/ssl/client-keystore.p12 -Djavax.net.ssl.keyStorePassword=${KEYSTORE_PASSWORD} -Djavax.net.ssl.trustStore=src/main/resources/ssl/client-truststore.p12 -Djavax.net.ssl.trustStorePassword=${TRUSTSTORE_PASSWORD} -jar target/redis-0.0.1-SNAPSHOT.jar 2 | -------------------------------------------------------------------------------- /scripts/updateTransactionStatus.sh: -------------------------------------------------------------------------------- 1 | # generate new transactions to move all of one transaction 2 | # Status up to the next transaction status. Parameter is target status. 3 | # Can choose SETTLED or POSTED 4 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/statusChangeTransactions/?transactionStatus=POSTED' 5 | -------------------------------------------------------------------------------- /scripts/addTag.sh: -------------------------------------------------------------------------------- 1 | # add a tag to a transaction. Tags allow user to mark transactions to be in a buckets such as Travel or Food for budgetary tracking purposes 2 | # date can be looked up and put in range 3 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/addTag/?transactionID=4484J&tag=Travel&operation=ADD' 4 | -------------------------------------------------------------------------------- /scripts/addTag2.sh: -------------------------------------------------------------------------------- 1 | # add a tag to a transaction. Tags allow user to mark transactions to be in a buckets such as Travel or Food for budgetary tracking purposes 2 | # date can be looked up and put in range 3 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/addTag/?transactionID=4484J&tag=Food&operation=ADD' 4 | -------------------------------------------------------------------------------- /scripts/getByCreditCard.sh: -------------------------------------------------------------------------------- 1 | # easiest to look up a credit card using redinsight and edit this script to find an existing credit card 2 | # date can be looked up and put in range 3 | curl -X GET -H "Content-Type: application/json" 'http://localhost:8080/creditCardTransactions/?creditCard=33c54179xb350x4b3dx9e96xa5be21c6539b&account=Acct1550J&from=2021-07-27&to=2021-09-30' 4 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/domain/Email.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.domain; 2 | import lombok.*; 3 | import java.io.Serializable; 4 | 5 | @Data 6 | @AllArgsConstructor 7 | @NoArgsConstructor 8 | @Getter 9 | @Setter 10 | 11 | public class Email implements Serializable { 12 | private String emailAddress; 13 | private String emailLabel; 14 | private String customerId; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/domain/Phone.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.domain; 2 | import lombok.*; 3 | 4 | import java.io.Serializable; 5 | 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | @Getter 11 | @Setter 12 | 13 | public class Phone implements Serializable { 14 | 15 | private String phoneNumber; 16 | private String phoneLabel; 17 | private String customerId; 18 | } 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | redis: 5 | image: redis/redis-stack:latest 6 | container_name: redis 7 | ports: 8 | - "6379:6379" 9 | volumes: 10 | - ./redis_data:/data 11 | 12 | insight: 13 | image: "redislabs/redisinsight:latest" 14 | container_name: insight 15 | ports: 16 | - "8001:8001" 17 | volumes: 18 | - ./redisinsight:/db 19 | depends_on: 20 | - redis 21 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/domain/TransactionReturn.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | 8 | import java.io.Serializable; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | 14 | public class TransactionReturn implements Serializable { 15 | 16 | private String reasonCode; 17 | private String reasonDescription; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/domain/Merchant.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.domain; 2 | 3 | import lombok.*; 4 | 5 | import java.io.Serializable; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | @Getter 11 | @Setter 12 | 13 | public class Merchant implements Serializable { 14 | 15 | private String name; 16 | private String categoryCode; 17 | private String categoryDescription; 18 | private String state; 19 | private String countryCode; 20 | } 21 | -------------------------------------------------------------------------------- /scripts/customer.json: -------------------------------------------------------------------------------- 1 | { "customerId": "cust001", "addressLine1": "4744 17th av s", "addressLine2": "", "addressType": "home", "billPayEnrolled": "", "city": "Minneapolis", "countryCode":"00", "createdBy":"jph", "customerOriginSystem":"IDR", "customerStatus":"A", "customerType":"BANK", "dateOfBirth":"1949.01.23", "firstName":"Bruno", "fullName":"Bruno Waldo Emerson", "gender":"M", "governmentId":"8887778989", "governmentIdType":"SSN", "lastName":"Emerson", "lastUpdatedBy":"jph", "middleName":"Waldo", "prefix":"MR", "queryHelperColumn":"help", "stateAbbreviation":"MN", "zipcode":"55444", "zipcode4":"55444-3322" } 2 | -------------------------------------------------------------------------------- /scripts/customer1.json: -------------------------------------------------------------------------------- 1 | { "customerId": "cust005", "addressLine1": "4744 17th av s", "addressLine2": "", "addressType": "home", "billPayEnrolled": "", "city": "Minneapolis", "countryCode":"00", "createdBy":"jph", "customerOriginSystem":"IDR", "customerStatus":"A", "customerType":"BANK", "dateOfBirth":"1949.01.23", "firstName":"Jorge", "fullName":"Jorge Waldo Emerson", "gender":"M", "governmentId":"8887778989", "governmentIdType":"SSN", "lastName":"Emerson", "lastUpdatedBy":"jph", "middleName":"Waldo", "prefix":"MR", "queryHelperColumn":"help", "stateAbbreviation":"MN", "zipcode":"55444", "zipcode4":"55444-3322" } 2 | -------------------------------------------------------------------------------- /scripts/customer3.json: -------------------------------------------------------------------------------- 1 | { "customerId": "cust002", "addressLine1": "4744 17th av s", "addressLine2": "", "addressType": "home", "billPayEnrolled": "", "city": "Minneapolis", "countryCode":"00", "createdBy":"jph", "customerOriginSystem":"IDR", "customerStatus":"A", "customerType":"BANK", "dateOfBirth":"1949.01.23", "firstName":"Susan", "fullName":"Susan Waldo Emerson", "gender":"M", "governmentId":"8887778989", "governmentIdType":"SSN", "lastName":"Emerson", "lastUpdatedBy":"jph", "middleName":"Waldo", "prefix":"MR", "queryHelperColumn":"help", "stateAbbreviation":"MN", "zipcode":"55444", "zipcode4":"55444-3322" } 2 | -------------------------------------------------------------------------------- /scripts/customer2.json: -------------------------------------------------------------------------------- 1 | { "customerId": "cust004", "addressLine1": "4744 17th av s", "addressLine2": "", "addressType": "home", "billPayEnrolled": "", "city": "Minneapolis", "countryCode":"00", "createdBy":"jph", "customerOriginSystem":"IDR", "customerStatus":"A", "customerType":"BANK", "dateOfBirth":"1949.01.23", "firstName":"Edward", "fullName":"Edward Waldo Emerson", "gender":"M", "governmentId":"8887778989", "governmentIdType":"SSN", "lastName":"Emerson", "lastUpdatedBy":"jph", "middleName":"Waldo", "prefix":"MR", "queryHelperColumn":"help", "stateAbbreviation":"MN", "zipcode":"55444", "zipcode4":"55444-3322" } 2 | -------------------------------------------------------------------------------- /scripts/customer4.json: -------------------------------------------------------------------------------- 1 | { "customerId": "cust003", "addressLine1": "4745 17th av s", "addressLine2": "", "addressType": "home", "billPayEnrolled": "", "city": "Minneapolis", "countryCode":"00", "createdBy":"jph", "customerOriginSystem":"IDR", "customerStatus":"A", "customerType":"BANK", "dateOfBirth":"1949.01.23", "firstName":"George", "fullName":"George Waldo Emerson", "gender":"M", "governmentId":"8887778989", "governmentIdType":"SSN", "lastName":"Emerson", "lastUpdatedBy":"jph", "middleName":"Waldo", "prefix":"MR", "queryHelperColumn":"help", "stateAbbreviation":"MN", "zipcode":"55444", "zipcode4":"55444-3322" } 2 | -------------------------------------------------------------------------------- /scripts/customer5.json: -------------------------------------------------------------------------------- 1 | { "customerId": "cust006", "addressLine1": "4744 17th av s", "addressLine2": "", "addressType": "home", "billPayEnrolled": "", "city": "Minneapolis", "countryCode":"00", "createdBy":"jph", "customerOriginSystem":"IDR", "customerStatus":"A", "customerType":"BANK", "dateOfBirth":"1949.01.23", "firstName":"Robert", "fullName":"Robert Waldo Emerson", "gender":"M", "governmentId":"8887778989", "governmentIdType":"SSN", "lastName":"Emerson", "lastUpdatedBy":"jph", "middleName":"Waldo", "prefix":"MR", "queryHelperColumn":"help", "stateAbbreviation":"MN", "zipcode":"55444", "zipcode4":"55444-3322" } 2 | -------------------------------------------------------------------------------- /scripts/customer6.json: -------------------------------------------------------------------------------- 1 | { "customerId": "cust007", "addressLine1": "4744 17th av s", "addressLine2": "", "addressType": "home", "billPayEnrolled": "", "city": "Minneapolis", "countryCode":"00", "createdBy":"jph", "customerOriginSystem":"IDR", "customerStatus":"A", "customerType":"BANK", "dateOfBirth":"1949.01.23", "firstName":"Oliver", "fullName":"Oliver Waldo Emerson", "gender":"M", "governmentId":"8887778989", "governmentIdType":"SSN", "lastName":"Emerson", "lastUpdatedBy":"jph", "middleName":"Waldo", "prefix":"MR", "queryHelperColumn":"help", "stateAbbreviation":"MN", "zipcode":"55444", "zipcode4":"55444-3322" } 2 | -------------------------------------------------------------------------------- /scripts/generateData.sh: -------------------------------------------------------------------------------- 1 | # This script uses an API call to generate sample banking customers, 2 | # accounts and transactions. It uses Spring ASYNC techniques to 3 | # generate higher load. A flag chooses between running the transactions 4 | # pipelined in Redis or in normal non-pipelined method. 5 | curl 'http://localhost:8080/generateData?noOfCustomers=500&noOfTransactions=10000&noOfDays=5&key_suffix=J&pipelined=false' 6 | # with ssl 7 | # curl -cacert='..//src/main/resources/ssl/proxy-cert.pem' 'http://localhost:8080/generateData?noOfCustomers=500&noOfTransactions=10000&noOfDays=5&key_suffix=J&pipelined=false' 8 | -------------------------------------------------------------------------------- /scripts/startAppServers.sh: -------------------------------------------------------------------------------- 1 | # start multiple app server instances for load testing 2 | nohup java -DServer.port=8080 -jar ../target/redis-0.0.1-SNAPSHOT.jar > /tmp/appServer8080.out 2>&1 & 3 | nohup java -DServer.port=8081 -jar ../target/redis-0.0.1-SNAPSHOT.jar > /tmp/appServer8081.out 2>&1 & 4 | nohup java -DServer.port=8082 -jar ../target/redis-0.0.1-SNAPSHOT.jar > /tmp/appServer8082.out 2>&1 & 5 | nohup java -DServer.port=8083 -jar ../target/redis-0.0.1-SNAPSHOT.jar > /tmp/appServer8083.out 2>&1 & 6 | nohup java -DServer.port=8084 -jar ../target/redis-0.0.1-SNAPSHOT.jar > /tmp/appServer8084.out 2>&1 & 7 | nohup java -DServer.port=8085 -jar ../target/redis-0.0.1-SNAPSHOT.jar > /tmp/appServer8085.out 2>&1 & 8 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.scheduling.annotation.EnableAsync; 8 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 9 | 10 | import java.util.concurrent.Executor; 11 | 12 | @SpringBootApplication 13 | 14 | public class DemoApplication extends SpringBootServletInitializer { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(DemoApplication.class, args); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/putCustomer.sh: -------------------------------------------------------------------------------- 1 | curl -X POST -H "Content-Type: application/json" http://localhost:8080/postCustomer?customer --data @customer.json 2 | curl -X POST -H "Content-Type: application/json" http://localhost:8080/postCustomer?customer --data @customer1.json 3 | curl -X POST -H "Content-Type: application/json" http://localhost:8080/postCustomer?customer --data @customer2.json 4 | curl -X POST -H "Content-Type: application/json" http://localhost:8080/postCustomer?customer --data @customer3.json 5 | curl -X POST -H "Content-Type: application/json" http://localhost:8080/postCustomer?customer --data @customer4.json 6 | curl -X POST -H "Content-Type: application/json" http://localhost:8080/postCustomer?customer --data @customer5.json 7 | curl -X POST -H "Content-Type: application/json" http://localhost:8080/postCustomer?customer --data @customer6.json 8 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/domain/Account.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.domain; 2 | 3 | import lombok.*; 4 | import org.springframework.data.annotation.Reference; 5 | import org.springframework.data.redis.core.RedisHash; 6 | import org.springframework.data.annotation.Id; 7 | import org.springframework.data.redis.core.index.Indexed; 8 | 9 | import java.io.Serializable; 10 | 11 | 12 | @Data 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | @Getter 16 | @Setter 17 | 18 | 19 | public class Account implements Serializable { 20 | private String accountNo; 21 | private String customerId; 22 | private String accountType; 23 | private String accountOriginSystem; 24 | private String accountStatus; 25 | private String cardNum; 26 | private Long openDatetime; 27 | private Long lastUpdated; 28 | private String lastUpdatedBy; 29 | private String createdBy; 30 | private Long createdDatetime; 31 | } 32 | -------------------------------------------------------------------------------- /scripts/generateLots.sh: -------------------------------------------------------------------------------- 1 | # for server testing to generate higher load levels. 2 | # Use with startAppservers.sh 3 | nohup curl 'http://localhost:8080/generateData?noOfCustomers=5000&noOfTransactions=100000&noOfDays=10&key_suffix=J&pipelined=false' > /tmp/generateJ.out 2>&1 & 4 | nohup curl 'http://localhost:8081/generateData?noOfCustomers=5000&noOfTransactions=100000&noOfDays=10&key_suffix=P&pipelined=false' > /tmp/generateP.out 2>&1 & 5 | nohup curl 'http://localhost:8082/generateData?noOfCustomers=5000&noOfTransactions=100000&noOfDays=10&key_suffix=H&pipelined=false' > /tmp/generateH.out 2>&1 & 6 | nohup curl 'http://localhost:8083/generateData?noOfCustomers=5000&noOfTransactions=100000&noOfDays=10&key_suffix=C&pipelined=false' > /tmp/generateC.out 2>&1 & 7 | nohup curl 'http://localhost:8084/generateData?noOfCustomers=5000&noOfTransactions=100000&noOfDays=10&key_suffix=A&pipelined=false' > /tmp/generateA.out 2>&1 & 8 | nohup curl 'http://localhost:8085/generateData?noOfCustomers=5000&noOfTransactions=100000&noOfDays=10&key_suffix=G&pipelined=false' > /tmp/generateG.out 2>&1 & 9 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/domain/Transaction.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.domain; 2 | 3 | import lombok.*; 4 | import org.springframework.data.annotation.Id; 5 | 6 | 7 | import java.io.Serializable; 8 | import java.util.Date; 9 | @Data 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | @Getter 13 | @Setter 14 | 15 | public class Transaction implements Serializable { 16 | private String tranId; 17 | private String accountNo; 18 | // debit or credit 19 | private String amountType; 20 | private String merchant; 21 | private String referenceKeyType; 22 | private String referenceKeyValue; 23 | private String originalAmount; 24 | private String amount; 25 | private String tranCd ; 26 | private String description; 27 | private Long initialDate; 28 | private Long settlementDate; 29 | private Long postingDate; 30 | // this is authorized, posted, settled 31 | private String status ; 32 | private String transactionReturn; 33 | private String location; 34 | private String transactionTags; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.redis.host=localhost 2 | spring.redis.port=6379 3 | # spring.redis.password=${REDIS_PASSWORD} 4 | spring.redis.ssl=true 5 | spring.redis.timeout=1000ms 6 | spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration 7 | app.numberOfRatings=5000 8 | app.ratingStars=5 9 | app.numberOfCarts=2500 10 | app.transactionSearchIndexName=Transaction 11 | app.customerSearchIndexName=Customer 12 | app.merchantSearchIndexName=Merchant 13 | app.accountSearchIndexName=Account 14 | app.emailSearchIndexName=Email 15 | app.phoneSearchIndexName=Phone 16 | app.transactionReturnSearchIndexName=TransactionReturn 17 | logging.level.org.springframework=INFO 18 | logging.level.com.jphaugla=DEBUG 19 | logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n 20 | #server.ssl.key-store=./src/main/resources/ssl/client-keystore.p12 21 | #server.ssl.key-store-password=${KEYSTORE_PASSWORD} 22 | #server.ssl.trust-store=./src/main/resources/ssl/client-truststore.p12 23 | #server.ssl.trust-store-password=${TRUSTSTORE_PASSWORD} 24 | 25 | -------------------------------------------------------------------------------- /marketplace.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_name": "Digital Banking using RedisTemplate and Redis Search", 3 | "description": "A Digital Banking application built using RedisTemplate and Redis Search", 4 | "type": "Building Block", 5 | "contributed_by": "Redis", 6 | "repo_url": "https://github.com/redis-developer/digitalbanking-redistemplate", 7 | "preview_image_url": "https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/app_preview_image.png", 8 | "download_url": "https://github.com/redis-developer/digitalbanking-redistemplate/archive/refs/heads/master.zip", 9 | "hosted_url": "", 10 | "quick_deploy": "false", 11 | "deploy_buttons": [], 12 | "language": [ 13 | "JavaScript" 14 | ], 15 | "redis_commands": [ 16 | "FT.CREATE", 17 | "HSET", 18 | "FT.SEARCH", 19 | "RPUSH", 20 | "EXPIRE", 21 | "LRANGE" 22 | ], 23 | "redis_use_cases": [ 24 | "Caching" 25 | ], 26 | "redis_features": [ 27 | "Search and Query" 28 | ], 29 | "app_image_urls": [], 30 | "youtube_url": "", 31 | "special_tags": [ 32 | "Hackathon" 33 | ], 34 | "verticals": [ 35 | "Retail" 36 | ], 37 | "markdown": "https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/README.md" 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/domain/Customer.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.domain; 2 | 3 | import lombok.*; 4 | import java.io.Serializable; 5 | 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | @Getter 11 | @Setter 12 | 13 | 14 | public class Customer implements Serializable { 15 | private String customerId; 16 | private String addressLine1; 17 | private String addressLine2; 18 | private String addressType; 19 | private String billPayEnrolled; 20 | private String city; 21 | private String countryCode; 22 | private String createdBy; 23 | private Long createdDatetime; 24 | private String customerOriginSystem; 25 | private String customerStatus; 26 | private String customerType; 27 | private String dateOfBirth; 28 | private String firstName; 29 | private String fullName; 30 | private String gender; 31 | private String governmentId; 32 | private String governmentIdType; 33 | private String lastName; 34 | private Long lastUpdated; 35 | private String lastUpdatedBy; 36 | private String middleName; 37 | private String prefix; 38 | private String queryHelperColumn; 39 | private String stateAbbreviation; 40 | private String zipcode; 41 | private String zipcode4; 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/boot/EmailIndex.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.boot; 2 | 3 | import com.redislabs.mesclun.RedisModulesCommands; 4 | import com.redislabs.mesclun.StatefulRedisModulesConnection; 5 | import com.redislabs.mesclun.search.CreateOptions; 6 | import com.redislabs.mesclun.search.Field; 7 | import io.lettuce.core.RedisCommandExecutionException; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.boot.CommandLineRunner; 12 | import org.springframework.core.annotation.Order; 13 | import org.springframework.stereotype.Component; 14 | 15 | @Component 16 | @Order(6) 17 | @Slf4j 18 | public class EmailIndex implements CommandLineRunner { 19 | 20 | @Autowired 21 | private StatefulRedisModulesConnection connection; 22 | 23 | @Value("${app.emailSearchIndexName}") 24 | private String emailSearchIndexName; 25 | 26 | @Override 27 | @SuppressWarnings({ "unchecked" }) 28 | public void run(String... args) throws Exception { 29 | RedisModulesCommands emailCommands = connection.sync(); 30 | try { 31 | emailCommands.indexInfo(emailSearchIndexName); 32 | } catch (RedisCommandExecutionException rcee) { 33 | if (rcee.getMessage().equals("Unknown Index name")) { 34 | 35 | CreateOptions options = CreateOptions.builder()// 36 | .prefix(emailSearchIndexName + ':').build(); 37 | 38 | Field emailAddress = Field.text("emailAddress").build(); 39 | Field customerId = Field.text("customerId").build(); 40 | emailCommands.create( 41 | emailSearchIndexName, // 42 | options, // 43 | emailAddress, customerId 44 | ); 45 | log.info(">>>> Created " + emailSearchIndexName + " Search Index..."); 46 | } 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/boot/PhoneIndex.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.boot; 2 | 3 | import com.redislabs.mesclun.RedisModulesCommands; 4 | import com.redislabs.mesclun.StatefulRedisModulesConnection; 5 | import com.redislabs.mesclun.search.CreateOptions; 6 | import com.redislabs.mesclun.search.Field; 7 | import io.lettuce.core.RedisCommandExecutionException; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.boot.CommandLineRunner; 12 | import org.springframework.core.annotation.Order; 13 | import org.springframework.stereotype.Component; 14 | 15 | @Component 16 | @Order(6) 17 | @Slf4j 18 | public class PhoneIndex implements CommandLineRunner { 19 | 20 | @Autowired 21 | private StatefulRedisModulesConnection connection; 22 | 23 | @Value("${app.phoneSearchIndexName}") 24 | private String phoneSearchIndexName; 25 | 26 | @Override 27 | @SuppressWarnings({ "unchecked" }) 28 | public void run(String... args) throws Exception { 29 | RedisModulesCommands phoneCommands = connection.sync(); 30 | try { 31 | phoneCommands.indexInfo(phoneSearchIndexName); 32 | } catch (RedisCommandExecutionException rcee) { 33 | if (rcee.getMessage().equals("Unknown Index name")) { 34 | 35 | CreateOptions options = CreateOptions.builder()// 36 | .prefix(phoneSearchIndexName + ':').build(); 37 | 38 | Field phoneNumber = Field.text("phoneNumber").build(); 39 | Field customerId = Field.text("customerId").build(); 40 | phoneCommands.create( 41 | phoneSearchIndexName, // 42 | options, // 43 | phoneNumber, customerId 44 | ); 45 | log.info(">>>> Created " + phoneSearchIndexName + " Search Index..."); 46 | } 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/boot/TransactionReturnIndex.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.boot; 2 | 3 | import com.redislabs.mesclun.RedisModulesCommands; 4 | import com.redislabs.mesclun.StatefulRedisModulesConnection; 5 | import com.redislabs.mesclun.search.CreateOptions; 6 | import com.redislabs.mesclun.search.Field; 7 | import io.lettuce.core.RedisCommandExecutionException; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.boot.CommandLineRunner; 12 | import org.springframework.core.annotation.Order; 13 | import org.springframework.stereotype.Component; 14 | 15 | @Component 16 | @Order(6) 17 | @Slf4j 18 | public class TransactionReturnIndex implements CommandLineRunner { 19 | 20 | @Autowired 21 | private StatefulRedisModulesConnection connection; 22 | 23 | @Value("${app.transactionReturnSearchIndexName}") 24 | private String transactionReturnSearchIndexName; 25 | 26 | @Override 27 | @SuppressWarnings({ "unchecked" }) 28 | public void run(String... args) throws Exception { 29 | RedisModulesCommands transactionReturnCommands = connection.sync(); 30 | try { 31 | transactionReturnCommands.indexInfo(transactionReturnSearchIndexName); 32 | } catch (RedisCommandExecutionException rcee) { 33 | if (rcee.getMessage().equals("Unknown Index name")) { 34 | 35 | CreateOptions options = CreateOptions.builder()// 36 | .prefix(transactionReturnSearchIndexName + ':').build(); 37 | 38 | Field reasonCode = Field.text("reasonCode").build(); 39 | Field reasonDescription = Field.text("reasonDescription").build(); 40 | transactionReturnCommands.create( 41 | transactionReturnSearchIndexName, // 42 | options, // 43 | reasonCode, reasonDescription 44 | ); 45 | log.info(">>>> Created " + transactionReturnSearchIndexName + " Search Index..."); 46 | } 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/boot/MerchantIndex.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.boot; 2 | 3 | import com.redislabs.mesclun.search.*; 4 | import com.redislabs.mesclun.StatefulRedisModulesConnection; 5 | import com.redislabs.mesclun.RedisModulesCommands; 6 | 7 | import io.lettuce.core.RedisCommandExecutionException; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.boot.CommandLineRunner; 12 | import org.springframework.core.annotation.Order; 13 | import org.springframework.stereotype.Component; 14 | 15 | @Component 16 | @Order(6) 17 | @Slf4j 18 | public class MerchantIndex implements CommandLineRunner { 19 | 20 | 21 | @Autowired 22 | private StatefulRedisModulesConnection connection; 23 | 24 | @Value("${app.merchantSearchIndexName}") 25 | private String merchantSearchIndexName; 26 | 27 | @Override 28 | @SuppressWarnings({ "unchecked" }) 29 | public void run(String... args) throws Exception { 30 | RedisModulesCommands merchantCommands = connection.sync(); 31 | try { 32 | merchantCommands.indexInfo(merchantSearchIndexName); 33 | } catch (RedisCommandExecutionException rcee) { 34 | if (rcee.getMessage().equals("Unknown Index name")) { 35 | 36 | CreateOptions options = CreateOptions.builder()// 37 | .prefix(merchantSearchIndexName + ':').build(); 38 | 39 | Field merchantName = Field.text("name").build(); 40 | Field categoryCode = Field.text("categoryCode").build(); 41 | Field categoryDescription = Field.text("categoryDescription").build(); 42 | Field merchantState = Field.text("state").build(); 43 | Field merchantCountry = Field.text("countryCode").build(); 44 | merchantCommands.create( 45 | merchantSearchIndexName, // 46 | options, // 47 | merchantName, categoryCode, categoryDescription, merchantState, merchantCountry 48 | ); 49 | log.info(">>>> Created " + merchantSearchIndexName + " Search Index..."); 50 | } 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/repository/PhoneRepository.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.repository; 2 | 3 | import com.jphaugla.domain.Phone; 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | 11 | import java.util.Map; 12 | import java.util.Optional; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Qualifier; 16 | import org.springframework.data.redis.core.RedisTemplate; 17 | 18 | import org.springframework.data.redis.core.StringRedisTemplate; 19 | import org.springframework.stereotype.Repository; 20 | @Repository 21 | 22 | public class PhoneRepository{ 23 | private static final String KEY = "Phone"; 24 | 25 | 26 | final Logger logger = LoggerFactory.getLogger(com.jphaugla.repository.PhoneRepository.class); 27 | ObjectMapper mapper = new ObjectMapper(); 28 | 29 | @Autowired 30 | @Qualifier("redisTemplateW1") 31 | private RedisTemplate redisTemplateW1; 32 | 33 | @Autowired 34 | private StringRedisTemplate stringRedisTemplate; 35 | 36 | public PhoneRepository() { 37 | 38 | logger.info("PhoneRepository constructor"); 39 | } 40 | 41 | public String create(Phone phone) { 42 | 43 | Map phoneHash = mapper.convertValue(phone, Map.class); 44 | redisTemplateW1.opsForHash().putAll("Phone:" + phone.getPhoneNumber(), phoneHash); 45 | // redisTemplate.opsForHash().putAll("Phone:" + phone.getPhoneId(), phoneHash); 46 | logger.info(String.format("Phone with ID %s saved", phone.getPhoneNumber())); 47 | return "Success\n"; 48 | } 49 | 50 | public Optional get(String phoneId) { 51 | logger.info("in Phone Repository.get with phone id=" + phoneId); 52 | String fullKey = "Phone:" + phoneId; 53 | Map phoneHash = stringRedisTemplate.opsForHash().entries(fullKey); 54 | logger.info("Full key is " + fullKey + " phoneHash is " + phoneHash); 55 | Phone phone = mapper.convertValue(phoneHash, Phone.class); 56 | logger.info("return phone " + phone.getPhoneNumber() + ":" + phone.getPhoneLabel() + ":" + phone.getCustomerId()); 57 | return Optional.ofNullable((phone)); 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.repository; 2 | 3 | import com.jphaugla.domain.Customer; 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | 11 | import java.util.Map; 12 | 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.beans.factory.annotation.Qualifier; 15 | import org.springframework.data.redis.core.RedisTemplate; 16 | 17 | import org.springframework.data.redis.core.StringRedisTemplate; 18 | import org.springframework.stereotype.Repository; 19 | @Repository 20 | 21 | public class CustomerRepository{ 22 | private static final String KEY = "Customer"; 23 | 24 | 25 | final Logger logger = LoggerFactory.getLogger(CustomerRepository.class); 26 | ObjectMapper mapper = new ObjectMapper(); 27 | 28 | @Autowired 29 | @Qualifier("redisTemplateW1") 30 | private RedisTemplate redisTemplateW1; 31 | 32 | @Autowired 33 | private StringRedisTemplate stringRedisTemplate; 34 | 35 | public CustomerRepository() { 36 | 37 | logger.info("CustomerRepository constructor"); 38 | } 39 | 40 | public String create(Customer customer) { 41 | if (customer.getCreatedDatetime() == null) { 42 | Long currentTimeMillis = System.currentTimeMillis(); 43 | customer.setCreatedDatetime(currentTimeMillis); 44 | customer.setLastUpdated(currentTimeMillis); 45 | } 46 | 47 | Map customerHash = mapper.convertValue(customer, Map.class); 48 | redisTemplateW1.opsForHash().putAll("Customer:" + customer.getCustomerId(), customerHash); 49 | // redisTemplate.opsForHash().putAll("Customer:" + customer.getCustomerId(), customerHash); 50 | logger.info(String.format("Customer with ID %s saved", customer.getCustomerId())); 51 | return "Success\n"; 52 | } 53 | 54 | public Customer get(String customerId) { 55 | logger.info("in CustomerRepository.get with customer id=" + customerId); 56 | String fullKey = "Customer:" + customerId; 57 | Map customerHash = stringRedisTemplate.opsForHash().entries(fullKey); 58 | Customer customer = mapper.convertValue(customerHash, Customer.class); 59 | return (customer); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/boot/CustomerIndex.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.boot; 2 | 3 | import com.redislabs.mesclun.search.*; 4 | import com.redislabs.mesclun.StatefulRedisModulesConnection; 5 | import com.redislabs.mesclun.RedisModulesCommands; 6 | 7 | import io.lettuce.core.RedisCommandExecutionException; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.boot.CommandLineRunner; 12 | import org.springframework.core.annotation.Order; 13 | import org.springframework.stereotype.Component; 14 | 15 | @Component 16 | @Order(6) 17 | @Slf4j 18 | public class CustomerIndex implements CommandLineRunner { 19 | 20 | @Autowired 21 | private StatefulRedisModulesConnection connection; 22 | 23 | @Value("${app.customerSearchIndexName}") 24 | private String customerSearchIndexName; 25 | 26 | @Override 27 | @SuppressWarnings({ "unchecked" }) 28 | public void run(String... args) throws Exception { 29 | RedisModulesCommands customerCommands = connection.sync(); 30 | try { 31 | customerCommands.indexInfo(customerSearchIndexName); 32 | } catch (RedisCommandExecutionException rcee) { 33 | if (rcee.getMessage().equals("Unknown Index name")) { 34 | 35 | CreateOptions options = CreateOptions.builder()// 36 | .prefix(customerSearchIndexName + ':').build(); 37 | 38 | Field city = Field.text("city").build(); 39 | Field firstName = Field.text("firstName").build(); 40 | Field fullName = Field.text("fullName").build(); 41 | Field lastName = Field.text("lastName").build(); 42 | Field stateAbbreviation = Field.text("stateAbbreviation").build(); 43 | Field zipcode = Field.text("zipcode").build(); 44 | Field customerId = Field.text( "customerId").build(); 45 | 46 | customerCommands.create( 47 | customerSearchIndexName, // 48 | options, // 49 | customerId, city, firstName, fullName, lastName, stateAbbreviation, zipcode 50 | ); 51 | 52 | log.info(">>>> Created " + customerSearchIndexName + " Search Index..."); 53 | } 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/repository/MerchantRepository.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.repository; 2 | 3 | import com.jphaugla.domain.Merchant; 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | 7 | import com.jphaugla.domain.Transaction; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.beans.factory.annotation.Qualifier; 17 | import org.springframework.data.redis.core.RedisTemplate; 18 | 19 | import org.springframework.data.redis.core.StringRedisTemplate; 20 | import org.springframework.stereotype.Repository; 21 | @Repository 22 | 23 | public class MerchantRepository{ 24 | private static final String KEY = "Merchant"; 25 | 26 | 27 | final Logger logger = LoggerFactory.getLogger(com.jphaugla.repository.MerchantRepository.class); 28 | ObjectMapper mapper = new ObjectMapper(); 29 | 30 | @Autowired 31 | @Qualifier("redisTemplateW1") 32 | private RedisTemplate redisTemplateW1; 33 | 34 | @Autowired 35 | private StringRedisTemplate stringRedisTemplate; 36 | 37 | public MerchantRepository() { 38 | 39 | logger.info("MerchantRepository constructor"); 40 | } 41 | 42 | public String create(Merchant merchant) { 43 | 44 | Map merchantHash = mapper.convertValue(merchant, Map.class); 45 | redisTemplateW1.opsForHash().putAll("Merchant:" + merchant.getName(), merchantHash); 46 | // redisTemplate.opsForHash().putAll("Merchant:" + merchant.getMerchantId(), merchantHash); 47 | logger.info(String.format("Merchant with ID %s saved", merchant.getName())); 48 | return "Success\n"; 49 | } 50 | 51 | public Merchant get(String merchantId) { 52 | logger.info("in MerchantRepository.get with merchant id=" + merchantId); 53 | String fullKey = "Merchant:" + merchantId; 54 | Map merchantHash = stringRedisTemplate.opsForHash().entries(fullKey); 55 | Merchant merchant = mapper.convertValue(merchantHash, Merchant.class); 56 | return (merchant); 57 | } 58 | public String createAll(List merchantList) { 59 | for (Merchant merchant : merchantList) { 60 | create(merchant); 61 | } 62 | return "Success\n"; 63 | } 64 | 65 | 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/boot/AccountIndex.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.boot; 2 | 3 | import com.redislabs.mesclun.RedisModulesCommands; 4 | import com.redislabs.mesclun.StatefulRedisModulesConnection; 5 | import com.redislabs.mesclun.search.CreateOptions; 6 | import com.redislabs.mesclun.search.Field; 7 | import io.lettuce.core.RedisCommandExecutionException; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.boot.CommandLineRunner; 12 | import org.springframework.core.annotation.Order; 13 | import org.springframework.stereotype.Component; 14 | 15 | @Component 16 | @Order(6) 17 | @Slf4j 18 | public class AccountIndex implements CommandLineRunner { 19 | 20 | @Autowired 21 | private StatefulRedisModulesConnection connection; 22 | 23 | @Value("${app.accountSearchIndexName}") 24 | private String accountSearchIndexName; 25 | 26 | @Override 27 | @SuppressWarnings({ "unchecked" }) 28 | public void run(String... args) throws Exception { 29 | RedisModulesCommands accountCommands = connection.sync(); 30 | try { 31 | accountCommands.indexInfo(accountSearchIndexName); 32 | } catch (RedisCommandExecutionException rcee) { 33 | if (rcee.getMessage().equals("Unknown Index name")) { 34 | 35 | CreateOptions options = CreateOptions.builder()// 36 | .prefix(accountSearchIndexName + ':').build(); 37 | 38 | Field customerId = Field.text("customerId").build(); 39 | Field accountType = Field.text("accountType").build(); 40 | Field accountOriginSystem = Field.text("accountOriginSystem").build(); 41 | Field accountStatus = Field.text("accountStatus").build(); 42 | Field cardNum = Field.text("cardNum").build(); 43 | Field openDate = Field.numeric("openDate").sortable(true).build(); 44 | Field lastUpdated = Field.numeric("lastUpdated").sortable(true).build(); 45 | accountCommands.create( 46 | accountSearchIndexName, // 47 | options, // 48 | customerId, accountType, accountOriginSystem, accountStatus, cardNum, openDate, lastUpdated 49 | ); 50 | log.info(">>>> Created " + accountSearchIndexName + " Search Index..."); 51 | } 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.repository; 2 | import com.jphaugla.domain.Account; 3 | 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | import com.jphaugla.domain.Account; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Qualifier; 16 | import org.springframework.data.redis.core.RedisTemplate; 17 | import org.springframework.data.redis.core.StringRedisTemplate; 18 | import org.springframework.stereotype.Repository; 19 | 20 | @Repository 21 | public class AccountRepository { 22 | private static final String KEY = "Account"; 23 | 24 | 25 | final Logger logger = LoggerFactory.getLogger(AccountRepository.class); 26 | ObjectMapper mapper = new ObjectMapper(); 27 | 28 | @Autowired 29 | @Qualifier("redisTemplateW1") 30 | private RedisTemplate redisTemplateW1; 31 | 32 | @Autowired 33 | private StringRedisTemplate stringRedisTemplate; 34 | 35 | public AccountRepository() { 36 | 37 | logger.info("AccountRepository constructor"); 38 | } 39 | 40 | public String create(Account account) { 41 | if (account.getCreatedDatetime() == null) { 42 | Long currentTimeMillis = System.currentTimeMillis(); 43 | account.setCreatedDatetime(currentTimeMillis); 44 | account.setOpenDatetime(currentTimeMillis); 45 | account.setLastUpdated(currentTimeMillis); 46 | } 47 | 48 | Map AccountHash = mapper.convertValue(account, Map.class); 49 | redisTemplateW1.opsForHash().putAll("Account:" + account.getAccountNo(), AccountHash); 50 | // redisTemplate.opsForHash().putAll("Account:" + Account.getAccountId(), AccountHash); 51 | logger.info(String.format("Account with ID %s saved", account.getAccountNo())); 52 | return "Success\n"; 53 | } 54 | 55 | public Account get(String accountId) { 56 | logger.info("in AccountRepository.get with Account id=" + accountId); 57 | String fullKey = "Account:" + accountId; 58 | Map AccountHash = stringRedisTemplate.opsForHash().entries(fullKey); 59 | Account account = mapper.convertValue(AccountHash, Account.class); 60 | return (account); 61 | } 62 | 63 | 64 | public void createAll(List accounts) { 65 | for (Account account : accounts) { 66 | create(account); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/repository/TransactionRepository.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.repository; 2 | 3 | import com.jphaugla.domain.Transaction; 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Qualifier; 16 | import org.springframework.data.redis.core.RedisTemplate; 17 | 18 | import org.springframework.data.redis.core.StringRedisTemplate; 19 | import org.springframework.stereotype.Repository; 20 | @Repository 21 | 22 | public class TransactionRepository{ 23 | private static final String KEY = "Transaction"; 24 | 25 | 26 | final Logger logger = LoggerFactory.getLogger(TransactionRepository.class); 27 | ObjectMapper mapper = new ObjectMapper(); 28 | 29 | @Autowired 30 | @Qualifier("redisTemplateW1") 31 | private RedisTemplate redisTemplateW1; 32 | 33 | @Autowired 34 | private StringRedisTemplate stringRedisTemplate; 35 | 36 | public TransactionRepository() { 37 | 38 | logger.info("TransactionRepository constructor"); 39 | } 40 | 41 | public String create(Transaction transaction) { 42 | if (transaction.getInitialDate() == null) { 43 | Long currentTimeMillis = System.currentTimeMillis(); 44 | transaction.setInitialDate(currentTimeMillis); 45 | } 46 | 47 | Map transactionHash = mapper.convertValue(transaction, Map.class); 48 | redisTemplateW1.opsForHash().putAll("Transaction:" + transaction.getTranId(), transactionHash); 49 | // redisTemplate.opsForHash().putAll("Transaction:" + transaction.getTransactionId(), transactionHash); 50 | // logger.info(String.format("Transaction with ID %s saved", transaction.getTranId())); 51 | return "Success\n"; 52 | } 53 | 54 | public String createAll(List transactionList) { 55 | for (Transaction transaction : transactionList) { 56 | create(transaction); 57 | } 58 | return "Success\n"; 59 | } 60 | 61 | public Transaction get(String transactionId) { 62 | logger.info("in TransactionRepository.get with transaction id=" + transactionId); 63 | String fullKey = "Transaction:" + transactionId; 64 | Map transactionHash = stringRedisTemplate.opsForHash().entries(fullKey); 65 | Transaction transaction = mapper.convertValue(transactionHash, Transaction.class); 66 | return (transaction); 67 | } 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/repository/TransactionReturnRepository.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.repository; 2 | 3 | import com.jphaugla.domain.Transaction; 4 | import com.jphaugla.domain.TransactionReturn; 5 | 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.beans.factory.annotation.Qualifier; 17 | import org.springframework.data.redis.core.RedisTemplate; 18 | 19 | import org.springframework.data.redis.core.StringRedisTemplate; 20 | import org.springframework.stereotype.Repository; 21 | @Repository 22 | 23 | public class TransactionReturnRepository{ 24 | private static final String KEY = "TransactionReturn"; 25 | 26 | 27 | final Logger logger = LoggerFactory.getLogger(TransactionReturnRepository.class); 28 | ObjectMapper mapper = new ObjectMapper(); 29 | 30 | @Autowired 31 | @Qualifier("redisTemplateW1") 32 | private RedisTemplate redisTemplateW1; 33 | 34 | @Autowired 35 | private StringRedisTemplate stringRedisTemplate; 36 | 37 | public TransactionReturnRepository() { 38 | 39 | logger.info("TransactionReturnRepository constructor"); 40 | } 41 | 42 | public String create(TransactionReturn transactionReturn) { 43 | Map transactionReturnHash = mapper.convertValue(transactionReturn, Map.class); 44 | redisTemplateW1.opsForHash().putAll("TransactionReturn:" + transactionReturn.getReasonCode(), transactionReturnHash); 45 | // redisTemplate.opsForHash().putAll("TransactionReturn:" + transactionReturn.getTransactionReturnId(), transactionReturnHash); 46 | logger.info(String.format("TransactionReturn with ID %s saved", transactionReturn.getReasonCode())); 47 | return "Success\n"; 48 | } 49 | public String createAll(List transactionReturnList) { 50 | for (TransactionReturn transactionReturn : transactionReturnList) { 51 | create(transactionReturn); 52 | } 53 | return "Success\n"; 54 | } 55 | 56 | public TransactionReturn get(String transactionReturnId) { 57 | logger.info("in TransactionReturnRepository.get with transactionReturn id=" + transactionReturnId); 58 | String fullKey = "TransactionReturn:" + transactionReturnId; 59 | Map transactionReturnHash = stringRedisTemplate.opsForHash().entries(fullKey); 60 | TransactionReturn transactionReturn = mapper.convertValue(transactionReturnHash, TransactionReturn.class); 61 | return (transactionReturn); 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/boot/TransactionIndex.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.boot; 2 | 3 | import com.redislabs.mesclun.search.*; 4 | import com.redislabs.mesclun.StatefulRedisModulesConnection; 5 | import com.redislabs.mesclun.RedisModulesCommands; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.CommandLineRunner; 10 | import org.springframework.core.annotation.Order; 11 | import org.springframework.stereotype.Component; 12 | 13 | import io.lettuce.core.RedisCommandExecutionException; 14 | 15 | 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | @Component 19 | @Order(6) 20 | @Slf4j 21 | public class TransactionIndex implements CommandLineRunner { 22 | 23 | @Autowired 24 | private StatefulRedisModulesConnection connection; 25 | 26 | @Value("${app.transactionSearchIndexName}") 27 | private String transactionSearchIndexName; 28 | 29 | 30 | @Override 31 | @SuppressWarnings({ "unchecked" }) 32 | public void run(String... args) throws Exception { 33 | 34 | RedisModulesCommands transactionCommands = connection.sync(); 35 | 36 | try { 37 | transactionCommands.indexInfo(transactionSearchIndexName); 38 | } catch (RedisCommandExecutionException rcee) { 39 | if (rcee.getMessage().equals("Unknown Index name")) { 40 | 41 | CreateOptions options = CreateOptions.builder()// 42 | .prefix(transactionSearchIndexName + ':').build(); 43 | 44 | Field accountNo = Field.text("accountNo").build(); 45 | Field amountType = Field.text("amountType").build(); 46 | Field merchant = Field.text("merchant").build(); 47 | Field status = Field.text("status").sortable(true).build(); 48 | Field description = Field.text("description").build(); 49 | Field referenceKeyType = Field.text("referenceKeyType").build(); 50 | Field referenceValue = Field.text("referenceValue").build(); 51 | Field tranCd = Field.text("tranCd").build(); 52 | Field location = Field.text("location").build(); 53 | Field transactionReturn = Field.text("transactionReturn").build(); 54 | Field initialDate = Field.numeric("initialDate").sortable(true).build(); 55 | Field settlementDate = Field.numeric("settlementDate").sortable(true).build(); 56 | Field postingDate = Field.numeric("postingDate").sortable(true).build(); 57 | Field transactionTags = Field.tag("transactionTags").separator(":").sortable(true).build(); 58 | 59 | transactionCommands.create( 60 | transactionSearchIndexName, // 61 | options, // 62 | accountNo, amountType, merchant, status, description, referenceKeyType, tranCd, location, transactionReturn, referenceValue, initialDate, settlementDate, postingDate,transactionTags 63 | ); 64 | 65 | log.info(">>>> Created " + transactionSearchIndexName + " Search Index..."); 66 | } 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/repository/EmailRepository.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.repository; 2 | 3 | import com.jphaugla.domain.Email; 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | 11 | import java.util.Map; 12 | import java.util.Set; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Qualifier; 16 | import org.springframework.data.redis.core.RedisTemplate; 17 | 18 | import org.springframework.data.redis.core.StringRedisTemplate; 19 | import org.springframework.stereotype.Repository; 20 | @Repository 21 | 22 | public class EmailRepository{ 23 | private static final String KEY = "Email"; 24 | 25 | 26 | final Logger logger = LoggerFactory.getLogger(com.jphaugla.repository.EmailRepository.class); 27 | ObjectMapper mapper = new ObjectMapper(); 28 | 29 | @Autowired 30 | @Qualifier("redisTemplateW1") 31 | private RedisTemplate redisTemplateW1; 32 | 33 | @Autowired 34 | private StringRedisTemplate stringRedisTemplate; 35 | 36 | public EmailRepository() { 37 | 38 | logger.info("EmailRepository constructor"); 39 | } 40 | 41 | public String create(Email email) { 42 | 43 | Map emailHash = mapper.convertValue(email, Map.class); 44 | redisTemplateW1.opsForHash().putAll("Email:" + email.getEmailAddress(), emailHash); 45 | // for demo purposed add a member to the set for the Customer 46 | stringRedisTemplate.opsForSet().add("CustEmail:" + email.getCustomerId(), email.getEmailAddress()); 47 | // redisTemplate.opsForHash().putAll("Email:" + email.getEmailId(), emailHash); 48 | logger.info(String.format("Email with ID %s saved", email.getEmailAddress())); 49 | return "Success\n"; 50 | } 51 | 52 | public Email get(String emailId) { 53 | logger.info("in EmailRepository.get with email id=" + emailId); 54 | String fullKey = "Email:" + emailId; 55 | Map emailHash = stringRedisTemplate.opsForHash().entries(fullKey); 56 | Email email = mapper.convertValue(emailHash, Email.class); 57 | return (email); 58 | } 59 | // this is sample code demonstrating removing all the emails for a customer without using redisearch 60 | public void delete(String emailId) { 61 | logger.info("in emailrepository.delete with emailId " + emailId); 62 | String fullKey = "Email:" + emailId; 63 | stringRedisTemplate.delete(fullKey); 64 | } 65 | 66 | public int deleteCustomerEmails (String customerId) { 67 | logger.info("in EmailRepository.deleteCustomerEmails with custid " + customerId); 68 | String custEmailKey = "CustEmail:"+ customerId; 69 | String fullEmailKey; 70 | Set emailsToDelete = stringRedisTemplate.opsForSet().members(custEmailKey); 71 | int emailCount = emailsToDelete.size(); 72 | for (String emailKey : emailsToDelete) { 73 | fullEmailKey = "Email:" + emailKey; 74 | logger.info("emailKey to delete is " + fullEmailKey); 75 | stringRedisTemplate.delete(fullEmailKey); 76 | } 77 | stringRedisTemplate.delete(custEmailKey); 78 | return emailCount; 79 | } 80 | 81 | 82 | } 83 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.6.5 9 | 10 | 11 | com.jphaugla 12 | redis 13 | 0.0.1-SNAPSHOT 14 | redis 15 | Demo project for Spring Boot with Redis 16 | jar 17 | 18 | 19 | com.jphaugla.DemoApplication 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-redis 26 | 27 | 28 | io.lettuce 29 | lettuce-core 30 | 31 | 32 | org.apache.commons 33 | commons-pool2 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-data-rest 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-web 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-test 46 | test 47 | 48 | 49 | org.projectlombok 50 | lombok 51 | 1.18.20 52 | provided 53 | 54 | 55 | joda-time 56 | joda-time 57 | 2.10.10 58 | 59 | 60 | com.redislabs 61 | spring-redis-modules 62 | 1.3.2 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-maven-plugin 71 | 72 | 73 | 74 | 75 | 76 | 77 | spring-milestones 78 | Spring Milestones 79 | https://repo.spring.io/milestone 80 | 81 | 82 | spring-snapshots 83 | Spring Snapshots 84 | https://repo.spring.io/snapshot 85 | 86 | true 87 | 88 | 89 | 90 | 91 | 92 | spring-milestones 93 | Spring Milestones 94 | https://repo.spring.io/milestone 95 | 96 | 97 | spring-snapshots 98 | Spring Snapshots 99 | https://repo.spring.io/snapshot 100 | 101 | true 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.config; 2 | 3 | import io.lettuce.core.ClientOptions; 4 | import io.lettuce.core.resource.ClientResources; 5 | import io.lettuce.core.resource.DefaultClientResources; 6 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig; 7 | import org.springframework.beans.factory.annotation.Qualifier; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.autoconfigure.data.redis.RedisProperties; 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 11 | import org.springframework.context.annotation.ComponentScan; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.context.annotation.Primary; 14 | 15 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 16 | import org.springframework.data.redis.connection.RedisConnectionFactory; 17 | import org.springframework.data.redis.connection.RedisStandaloneConfiguration; 18 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 19 | import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; 20 | import org.springframework.data.redis.core.RedisTemplate; 21 | import org.springframework.data.redis.core.StringRedisTemplate; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; 24 | import org.springframework.data.redis.connection.RedisPassword; 25 | import org.springframework.core.env.Environment; 26 | 27 | import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; 28 | import org.springframework.data.redis.serializer.GenericToStringSerializer; 29 | import org.springframework.data.redis.serializer.StringRedisSerializer; 30 | import org.springframework.scheduling.annotation.EnableAsync; 31 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 32 | 33 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 34 | import org.springframework.core.task.TaskExecutor; 35 | import org.springframework.context.annotation.Bean; 36 | import java.util.concurrent.Executor; 37 | import java.time.Duration; 38 | 39 | @Configuration 40 | @EnableConfigurationProperties(RedisProperties.class) 41 | @EnableAsync 42 | @EnableRedisRepositories 43 | 44 | @ComponentScan("com.jphaugla") 45 | public class RedisConfig { 46 | @Autowired 47 | private Environment env; 48 | private @Value("${spring.redis.timeout}") 49 | Duration redisCommandTimeout; 50 | 51 | @Bean(name = "redisConnectionFactory") 52 | @Primary 53 | public LettuceConnectionFactory redisConnectionFactory() { 54 | // LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() 55 | // .commandTimeout(redisCommandTimeout).poolConfig(new GenericObjectPoolConfig()).build(); 56 | LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() 57 | .commandTimeout(redisCommandTimeout).build(); 58 | RedisStandaloneConfiguration redisServerConf = new RedisStandaloneConfiguration(); 59 | redisServerConf.setHostName(env.getProperty("spring.redis.host")); 60 | redisServerConf.setPort(Integer.parseInt(env.getProperty("spring.redis.port"))); 61 | if(env.getProperty("spring.redis.password") != null && !env.getProperty("spring.redis.password").isEmpty()) { 62 | redisServerConf.setPassword(RedisPassword.of(env.getProperty("spring.redis.password"))); 63 | } 64 | return new LettuceConnectionFactory(redisServerConf, clientConfig); 65 | } 66 | 67 | @Bean 68 | @Primary 69 | public RedisTemplate redisTemplateW1(@Qualifier("redisConnectionFactory") RedisConnectionFactory redisConnectionFactory) { 70 | RedisTemplate redisTemplate = new RedisTemplate<>(); 71 | redisTemplate.setConnectionFactory(redisConnectionFactory); 72 | redisTemplate.setHashKeySerializer(new StringRedisSerializer()); 73 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 74 | redisTemplate.setHashValueSerializer(new GenericToStringSerializer(Long.class)); 75 | // redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer(Object.class)); 76 | redisTemplate.afterPropertiesSet(); 77 | return redisTemplate; 78 | } 79 | 80 | 81 | @Bean("threadPoolTaskExecutor") 82 | public TaskExecutor getAsyncExecutor() { 83 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 84 | // on large 64 core machine, drove setCorePoolSize to 200 to really spike performance 85 | executor.setCorePoolSize(20); 86 | executor.setMaxPoolSize(1000); 87 | executor.setWaitForTasksToCompleteOnShutdown(true); 88 | executor.setThreadNamePrefix("Async-"); 89 | return executor; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/service/AsyncService.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.service; 2 | 3 | import com.jphaugla.domain.*; 4 | import com.jphaugla.repository.*; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.dao.DataAccessException; 7 | import org.springframework.data.redis.connection.RedisConnection; 8 | 9 | import org.springframework.data.redis.core.RedisCallback; 10 | import org.springframework.data.redis.core.StringRedisTemplate; 11 | import org.springframework.data.redis.hash.HashMapper; 12 | import org.springframework.data.redis.hash.ObjectHashMapper; 13 | import org.springframework.scheduling.annotation.Async; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.concurrent.CompletableFuture; 19 | import java.util.concurrent.ExecutionException; 20 | 21 | 22 | 23 | @Service 24 | public class AsyncService { 25 | 26 | @Autowired 27 | private AccountRepository accountRepository; 28 | @Autowired 29 | private CustomerRepository customerRepository; 30 | @Autowired 31 | private TransactionRepository transactionRepository; 32 | @Autowired 33 | private PhoneRepository phoneRepository; 34 | @Autowired 35 | private EmailRepository emailRepository; 36 | @Autowired 37 | private StringRedisTemplate redisTemplate; 38 | 39 | @Async("threadPoolTaskExecutor") 40 | public CompletableFuture writeAllTransaction(List transactions) { 41 | transactionRepository.createAll(transactions); 42 | return CompletableFuture.completedFuture(0); 43 | } 44 | @Async("threadPoolTaskExecutor") 45 | public CompletableFuture writeTransaction(Transaction transaction) { 46 | transactionRepository.create(transaction); 47 | return CompletableFuture.completedFuture(0); 48 | } 49 | 50 | @Async("threadPoolTaskExecutor") 51 | public CompletableFuture writeAllAccounts(List accounts){ 52 | // Integer count = accounts.size(); 53 | accountRepository.createAll(accounts); 54 | return CompletableFuture.completedFuture(0); 55 | } 56 | 57 | @Async("threadPoolTaskExecutor") 58 | public CompletableFuture writeAccounts(Account account){ 59 | // Integer count = accounts.size(); 60 | accountRepository.create(account); 61 | return CompletableFuture.completedFuture(0); 62 | } 63 | 64 | @Async("threadPoolTaskExecutor") 65 | public CompletableFuture writeCustomer(Customer customer) { 66 | customerRepository.create(customer); 67 | return CompletableFuture.completedFuture(0); 68 | } 69 | 70 | @Async("threadPoolTaskExecutor") 71 | public CompletableFuture writePhone(Phone phoneNumber) { 72 | phoneRepository.create(phoneNumber); 73 | return CompletableFuture.completedFuture(0); 74 | } 75 | 76 | @Async("threadPoolTaskExecutor") 77 | public CompletableFuture writeEmail(Email email) { 78 | emailRepository.create(email); 79 | return CompletableFuture.completedFuture(0); 80 | } 81 | 82 | public void writeTransactionList(List transactionList) { 83 | 84 | HashMapper mapper = new ObjectHashMapper(); 85 | this.redisTemplate.executePipelined(new RedisCallback() { 86 | @Override 87 | public Object doInRedis(RedisConnection connection) 88 | throws DataAccessException { 89 | connection.openPipeline(); 90 | for (Transaction tx : transactionList) { 91 | String hashName = "Transaction:" + tx.getTranId(); 92 | Map mappedHash = mapper.toHash(tx); 93 | connection.hMSet(hashName.getBytes(), mappedHash); 94 | } 95 | connection.closePipeline(); 96 | return null; 97 | } 98 | }); 99 | } 100 | 101 | 102 | public void writePostedDateIndex(List transactionList) { 103 | 104 | HashMapper mapper = new ObjectHashMapper(); 105 | this.redisTemplate.executePipelined(new RedisCallback() { 106 | @Override 107 | public Object doInRedis(RedisConnection connection) 108 | throws DataAccessException { 109 | connection.openPipeline(); 110 | 111 | for (Transaction tx : transactionList) { 112 | if(tx.getPostingDate() != null) { 113 | String keyname="Trans:PostDate:" + tx.getAccountNo(); 114 | connection.zAdd(keyname.getBytes(), tx.getPostingDate(), 115 | tx.getTranId().getBytes()); 116 | } 117 | } 118 | connection.closePipeline(); 119 | return null; 120 | } 121 | }); 122 | 123 | } 124 | 125 | 126 | @Async("threadPoolTaskExecutor") 127 | public CompletableFuture writeAccountTransactions (List transactionList) throws IllegalAccessException, ExecutionException, InterruptedException { 128 | 129 | // writes a sorted set to be used as the posted date index 130 | // logger.info("entering writeAccountTransactions with list size of " + transactionList.size()); 131 | writeTransactionList(transactionList); 132 | // writePostedDateIndex(transactionList); 133 | // write using redisTemplate 134 | for (Transaction transaction:transactionList) { 135 | String hashName = "Transaction:" + transaction.getTranId(); 136 | String idxSetName = hashName + ":idx"; 137 | String merchantIndexName = "Transaction:merchant:" + transaction.getMerchant(); 138 | String accountIndexName = "Transaction:account:" + transaction.getAccountNo(); 139 | String statusIndexName = "Transaction:status:" + transaction.getStatus(); 140 | } 141 | return CompletableFuture.completedFuture(0); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/controller/BankingController.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.controller; 2 | 3 | import java.text.ParseException; 4 | import java.util.*; 5 | import java.util.concurrent.ExecutionException; 6 | 7 | import com.jphaugla.domain.*; 8 | 9 | import com.redislabs.mesclun.search.AggregateResults; 10 | import com.redislabs.mesclun.search.SearchResults; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.format.annotation.DateTimeFormat; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import com.jphaugla.service.BankService; 18 | 19 | 20 | @RestController 21 | public class BankingController { 22 | 23 | @Autowired 24 | private BankService bankService = BankService.getInstance(); 25 | 26 | private static final Logger logger = LoggerFactory.getLogger(BankingController.class); 27 | // customer 28 | @RequestMapping("/save_customer") 29 | public String saveCustomer() throws ParseException { 30 | bankService.saveSampleCustomer(); 31 | return "Done"; 32 | } 33 | 34 | 35 | // account 36 | @RequestMapping("/save_account") 37 | public String saveAccount() throws ParseException { 38 | bankService.saveSampleAccount(); 39 | return "Done"; 40 | } 41 | 42 | // transaction 43 | @RequestMapping("/save_transaction") 44 | public String saveTransaction() throws ParseException { 45 | bankService.saveSampleTransaction(); 46 | return "Done"; 47 | } 48 | 49 | @GetMapping("/generateData") 50 | @ResponseBody 51 | public String generateData (@RequestParam Integer noOfCustomers, @RequestParam Integer noOfTransactions, 52 | @RequestParam Integer noOfDays, @RequestParam String key_suffix, 53 | @RequestParam Boolean pipelined) 54 | throws ParseException, ExecutionException, InterruptedException, IllegalAccessException { 55 | 56 | bankService.generateData(noOfCustomers, noOfTransactions, noOfDays, key_suffix, pipelined); 57 | 58 | return "Done"; 59 | } 60 | 61 | @GetMapping("/testPipeline") 62 | @ResponseBody 63 | public String testPipeline (@RequestParam Integer noOfRecords) 64 | throws ParseException, ExecutionException, InterruptedException, IllegalAccessException { 65 | 66 | bankService.testPipeline(noOfRecords); 67 | 68 | return "Done"; 69 | } 70 | @GetMapping("/customerByPhone") 71 | 72 | public Customer getCustomerByPhone(@RequestParam String phoneString) { 73 | logger.debug("In get customerByPhone with phone as " + phoneString); 74 | return bankService.getCustomerByPhone(phoneString); 75 | } 76 | 77 | @GetMapping("/customerByEmail") 78 | 79 | public Customer getCustomerByEmail(@RequestParam String email) { 80 | logger.debug("IN get customerByEmail, email is " + email); 81 | return bankService.getCustomerByEmail(email); 82 | } 83 | 84 | @GetMapping("/customerByStateCity") 85 | 86 | public SearchResults getCustomerByStateCity(@RequestParam String state, @RequestParam String city) { 87 | logger.debug("IN get customerByState with state as " + state + " and city=" + city); 88 | return bankService.getCustomerByStateCity(state, city); 89 | } 90 | @GetMapping("/customerByZipcodeLastname") 91 | 92 | public SearchResults getCustomerIdsbyZipcodeLastname(@RequestParam String zipcode, @RequestParam String lastname) { 93 | logger.debug("IN get getCustomerIdsbyZipcodeLastname with zipcode as " + zipcode + " and lastname=" + lastname); 94 | return bankService.getCustomerIdsbyZipcodeLastname(zipcode, lastname); 95 | } 96 | @GetMapping("/merchantCategoryTransactions") 97 | 98 | public SearchResults getMerchantCategoryTransactions 99 | (@RequestParam String merchantCategory, @RequestParam String account, 100 | @RequestParam(name = "from") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date startDate, 101 | @RequestParam(name = "to") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date endDate) 102 | throws ParseException { 103 | logger.debug("In getMerchantCategoryTransactions merchantCategory=" + merchantCategory + " account=" + account + 104 | " from=" + startDate + " to=" + endDate); 105 | return bankService.getMerchantCategoryTransactions(merchantCategory, account, startDate, endDate); 106 | } 107 | @GetMapping("/merchantTransactions") 108 | 109 | public SearchResults getMerchantTransactions 110 | (@RequestParam String merchant, @RequestParam String account, 111 | @RequestParam(name = "from") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date startDate, 112 | @RequestParam(name = "to") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date endDate) 113 | throws ParseException { 114 | logger.info("In getMerchantTransactions merchant=" + merchant + " account=" + account + 115 | " from=" + startDate + " to=" + endDate); 116 | return bankService.getMerchantTransactions(merchant, account, startDate, endDate); 117 | } 118 | 119 | @GetMapping ("/transactionStatusReport") 120 | 121 | public AggregateResults transactionStatusReport () { 122 | AggregateResults keycounts = new AggregateResults<>(); 123 | return bankService.transactionStatusReport(); 124 | 125 | } 126 | @GetMapping("/returned_transactions") 127 | 128 | public SearchResults getReturnedTransaction () { 129 | logger.info("in bankcontroller getReturnedTransaction"); 130 | return bankService.getTransactionReturns(); 131 | } 132 | 133 | @GetMapping("/statusChangeTransactions") 134 | 135 | public AggregateResults generateStatusChangeTransactions(@RequestParam String transactionStatus) 136 | throws ParseException, IllegalAccessException, ExecutionException, InterruptedException { 137 | logger.info("generateStatusChangeTransactions transactionStatus=" + transactionStatus); 138 | AggregateResults changeReport = new AggregateResults<>(); 139 | 140 | changeReport.addAll(transactionStatusReport()); 141 | bankService.transactionStatusChange(transactionStatus); 142 | changeReport.addAll(transactionStatusReport()); 143 | 144 | return changeReport; 145 | 146 | } 147 | 148 | @GetMapping("/creditCardTransactions") 149 | 150 | public SearchResults getCreditCardTransactions 151 | (@RequestParam String creditCard, 152 | @RequestParam(name = "from") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date startDate, 153 | @RequestParam(name = "to") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date endDate) 154 | throws ParseException { 155 | logger.debug("getCreditCardTransactions creditCard=" + creditCard + 156 | " startDate=" + startDate + " endDate=" + endDate); 157 | return bankService.getCreditCardTransactions(creditCard, startDate, endDate); 158 | } 159 | 160 | @GetMapping("/accountTransactions") 161 | 162 | public SearchResults getAccountTransactions 163 | (@RequestParam String accountNo, 164 | @RequestParam(name = "from") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date startDate, 165 | @RequestParam(name = "to") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date endDate) 166 | throws ParseException { 167 | logger.debug("getCreditCardTransactions creditCard=" + accountNo + 168 | " startDate=" + startDate + " endDate=" + endDate); 169 | return bankService.getAccountTransactions(accountNo, startDate, endDate); 170 | 171 | } 172 | 173 | 174 | @GetMapping("/addTag") 175 | 176 | public void addTag(@RequestParam String transactionID, 177 | @RequestParam String tag, @RequestParam String operation) { 178 | logger.debug("addTags with transactionID=" + transactionID + " tag is " + tag + " operation is " + operation); 179 | bankService.addTag(transactionID, tag, operation); 180 | } 181 | 182 | @GetMapping("/getTags") 183 | public HashSet getTransactionTagList(@RequestParam String transactionID) { 184 | logger.debug("getTags with transactionID=" + transactionID); 185 | return bankService.getTransactionTagList(transactionID); 186 | } 187 | 188 | @GetMapping("/getTaggedTransactions") 189 | 190 | public SearchResults getTaggedTransactions 191 | (@RequestParam String accountNo, @RequestParam String tag) 192 | throws ParseException { 193 | logger.debug("In getTaggedTransactions accountNo=" + accountNo + " tag=" + tag ); 194 | return bankService.getTaggedTransactions(accountNo, tag); 195 | } 196 | 197 | @GetMapping("/getTransaction") 198 | public Transaction getTransaction(@RequestParam String transactionID) { 199 | Transaction transaction = bankService.getTransaction(transactionID); 200 | return transaction; 201 | } 202 | 203 | 204 | @GetMapping("/customer") 205 | 206 | public Optional getCustomer(@RequestParam String customerId) { 207 | return bankService.getCustomer(customerId); 208 | } 209 | 210 | @GetMapping("/deleteCustomer") 211 | 212 | public int deleteCustomer(@RequestParam String customerString) { 213 | return bankService.deleteCustomer(customerString); 214 | } 215 | 216 | @GetMapping("/deleteCustomerEmail") 217 | 218 | public int deleteCustomerEmail(@RequestParam String customerId) { 219 | return bankService.deleteCustomerEmail(customerId); 220 | } 221 | 222 | @PostMapping(value = "/postCustomer", consumes = "application/json", produces = "application/json") 223 | public String postCustomer(@RequestBody Customer customer ) throws ParseException { 224 | bankService.postCustomer(customer); 225 | return "Done\n"; 226 | } 227 | 228 | 229 | 230 | 231 | 232 | 233 | } 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Simple Digital Banking app 2 | 3 | ![image112](https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/digitalbanking.png) 4 | 5 | 6 | Provides a quick-start example of using Redis with springBoot with Banking structures. Digital Banking uses an API microservices approach to enable high speed requests for account, customer and transaction information. As seen below, this data is useful for a variety of business purposes in the bank. 7 | 8 | 9 | ![image112](https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/images/DigitalBanking.png) 10 | 11 | 12 | ### Note: 13 | 14 | This is the same as Redisearch-Digital-Banking but uses redistemplate instead of any of the crudrepository indexes. Redis Search2.0 indexes will be used. This is not using the crudrepository for the basic redis data. 15 | 16 | ## Overview 17 | 18 | In this tutorial, a java spring boot application is run through a jar file to support typical API calls to a REDIS banking data layer. A Redis Docker configuration is included. 19 | 20 | ## Redis Advantages for Digital Banking 21 | 22 | * Redis easily handles high write transaction volume 23 | * Redis has no tombstone issues and can upsert posted transactions over pending 24 | * Redis Enterprise scales vertically (large nodes) and horizontally (many nodes) 25 | *Redis Search2.0 automatically indexes the hash structure created by Spring Java CRUD repository 26 | 27 | ## Requirements 28 | 29 | * Docker installed on your local system, see [Docker Installation Instructions](https://docs.docker.com/engine/installation/). 30 | * Alternatively, can run Redis Enterprise and set the redis host and port in the application.properties file 31 | * When using Docker for Mac or Docker for Windows, the default resources allocated to the linux VM running docker are 2GB RAM and 2 CPU's. Make sure to adjust these resources to meet the resource requirements for the containers you will be running. More information can be found here on adjusting the resources allocated to docker. 32 | 33 | - [Docker Desktop for Mac](https://docs.docker.com/docker-for-mac/#advanced) 34 | - [Docker Desktop for Windows](https://docs.docker.com/docker-for-windows/#advanced) 35 | 36 | ## Links that help! 37 | 38 | * [Redis Stack](https://redis.com/blog/introducing-redis-stack/) 39 | * [Redis Search](https://redis.io/docs/stack/search/) 40 | * [RedisInsight](https://redis.io/docs/stack/insight/) 41 | * [Spring data for Redis GitHub](https://github.com/spring-projects/spring-data-examples/tree/master/redis/repositories) 42 | * [Spring data for Redis sample code](https://www.oodlestechnologies.com/blogs/Using-Redis-with-CrudRepository-in-Spring-Boot/) 43 | * [Lettuce tips Redis Spring boot](https://www.bytepitch.com/blog/redis-integration-spring-boot/) 44 | * [Spring data Reference in domain](https://github.com/spring-projects/spring-data-examples/blob/master/redis/repositories/src/main/java/example/springdata/redis/repositories/Person.java) 45 | * [spring data reference test code](https://github.com/spring-projects/spring-data-examples/blob/master/redis/repositories/src/test/java/example/springdata/redis/repositories/PersonRepositoryTests.java) 46 | * [spring async tips](https://dzone.com/articles/effective-advice-on-spring-async-part-1) 47 | * [brewdis sample application](https://github.com/redis-developer/brewdis) 48 | * [redis-developer lettucemod mesclun](https://github.com/redis-developer/lettucemod) 49 | 50 | 51 | ## Technical Overview 52 | 53 | This GitHub Java code uses the "mesclun" library for Redis modules. The mesclun library supports Redis Search, RedisGears andRedis Time Series. The original GitHub only used Spring Java without Redis Search. The repository is still intact at [this Github location](https://github.com/jphaugla/Redis-Digital-Banking). Another subsequent version uses crud repository and search at [this github location](https://github.com/jphaugla/Redisearch-Digital-Banking) 54 | 55 | All of the Spring Java indexes have been removed in this version. All the CRUD repository will also be removed in this when it is complete. 56 | Can also use TLS with Spring Boot java lettuce. Steps are near bottom. 57 | 58 | ### The spring java code 59 | 60 | This is basic spring links 61 | 62 | * [Spring Redis](https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#redis.repositories.indexes) 63 | * *boot*-Contains index creation for each of the fourRedis Searchindexes used in this solution: Account, Customer, Merchant, and Transaction 64 | * *config*-Initial configuration module using autoconfiguration and a threadpool sizing to adjust based on machine size 65 | * *controller*-http API call interfaces 66 | * *data*-code to generate POC type of customer, account, and transaction code 67 | * *domain*-has each of the java objects with their columns. Enables all the getter/setter methods 68 | * *repository*-has CRUD repository definitions. With transition toRedis Search2.0, not used as heavily as previously. This is where the redistemplate code is added if crud repository is no longer used. 69 | * *service*-asyncservice and bankservice doing the interaction with redis 70 | 71 | ### 72 | The java code demonstrates common API actions with the data layer in REDIS. The java spring Boot framework minimizes the amount of code to build and maintain this solution. Maven is used to build the java code and the code is deployed to the tomcat server. 73 | 74 | ### Data Structures in use 75 | 76 | ![image4](https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/images/Tables.png) 77 | 78 | ## Getting Started using Docker Desktop 79 | 80 | ## Prerequisites 81 | 82 | - Install Docker Desktop for Mac/Windows 83 | 84 | 85 | ## Cloning the repository 86 | 87 | 88 | ```bash 89 | git clone https://github.com/redis-developer/digitalbanking-redistemplate 90 | ``` 91 | 92 | 3. Refer to the notes for Redis Docker images used but don't get too bogged down as docker compose handles everything except for a few admin steps on tomcat. 93 | 94 | * [Redis Stack docker instructions](https://redis.io/docs/stack/get-started/install/docker/) 95 | 96 | 97 | ## Bring up the microservices 98 | 99 | Open terminal and change to the github home where you will see the docker-compose.yml file, then: 100 | 101 | ```bash 102 | docker-compose up -d 103 | ``` 104 | 105 | ## Getting Started without Docker on Ubuntu OS 106 | 107 | ### 1. Install Maven and Java 108 | 109 | ```bash 110 | sudo apt-get install maven 111 | sudo apt-get install default-jdk 112 | ``` 113 | 114 | ### 2. Clone the Repository 115 | 116 | ```bash 117 | git clone https://github.com/jphaugla/Redisearch-Digital-Banking.git 118 | ``` 119 | 120 | 1. Edit ./src/main/resources/application.properties to change the redis host and the redis port number 121 | 122 | ### 3. Execute the Sample application 123 | 124 | 1. Compile the code 125 | 126 | ```bash 127 | mvn package 128 | ``` 129 | 2. Run the JAR file. 130 | 131 | ```bash 132 | java -jar target/redis-0.0.1-SNAPSHOT.jar 133 | ``` 134 | 3. Test the application from a separate terminal window. This script uses an API call to generate sample banking customers, accounts and transactions. It uses Spring ASYNC techniques to generate higher load. A flag chooses between running the transactions pipelined in Redis or in normal non-pipelined method. 135 | 136 | ```bash 137 | ./scripts/generateData.sh 138 | ``` 139 | 140 | Shows a benchmark test run of generateData.sh on GCP servers. Although, this test run is usingRedis Search1.0 code base. Need to rerun this test. 141 | 142 | ![image2](https://raw.githubusercontent.com/redis-developer/digitalbanking-redistemplate/master/images/Benchmark.png) 143 | 144 | 4. Investigate the APIs in ./scripts. Adding theRedis Searchqueries behind each script here also... 145 | * addTag.sh - add a tag to a transaction. Tags allow user to mark transactions to be in a buckets such as Travel or Food for budgetary tracking purposes 146 | * deleteCustomer.sh - delete all customers matching a string 147 | * generateData.sh - simple API to generate default customer, accounts, merchants, phone numbers, emails and transactions 148 | * generateLots.sh - for server testing to generate higher load levels. Use with startAppservers.sh. Not for use with docker setup. This is load testing with redis enterprise and client application running in same network in the cloud. 149 | * getByAccount.sh - find transactions for an account between a date range 150 | * getByCreditCard.sh - find transactions for a credit card between a date range 151 | * getByCustID.sh - retrieve transactions for customer 152 | * getByEmail.sh - retrieve customer record using email address 153 | * getByMerchant.sh - find all transactions for an account from one merchant for date range 154 | * getByMerchantCategory.sh - find all transactions for an account from merchant category for date range 155 | * getByNamePhone.sh - get customers by phone and full name. 156 | * getByPhone.sh - get customers by phone only 157 | * getByStateCity.sh - get customers by city and state 158 | * getByZipLastname.sh - get customers by zipcode and lastname. 159 | * getReturns.sh - get returned transactions count by reason code 160 | * getTags.sh - get all tags on an account 161 | * getTaggedAccountTransactions.sh - find transactions for an account with a particular tag 162 | * getTransaction.sh - get one transaction by its transaction ID 163 | * getTransactionStatus.sh - see count of transactions by account status of PENDING, AUTHORIZED, SETTLED 164 | * putCustomer.sh - put a set of json customer records 165 | * saveAccount.sh - save a sample account 166 | * saveCustomer.sh - save a sample customer 167 | * saveTransaction.sh - save a sample Transaction 168 | * startAppservers.sh - start multiple app server instances for load testing 169 | * testPipeline.sh - test pipelining 170 | * updateTransactionStatus.sh - generate new transactions to move all transactions from one transaction Status up to the next transaction status. Parameter is target status. Can choose SETTLED or POSTED. Will move 100,000 transactions per call 171 | 172 | ## TLS with spring boot java lettuce 173 | 174 | Must set up the server side and verify server side Redis enterprise keys are working. This guide 175 | Really just a few steps to make this work and is not in the source code 176 | [This blog helps with TLS configuration with Redis Enterprise](https://tgrall.github.io/blog/2020/01/02/how-to-use-ssl-slash-tls-with-redis-enterprise/) 177 | Additional note, instead of using stunnel for testing redis-cli, see command after environment is established 178 | 179 | 180 | * Change environment variables for subsequent scripts 181 | 182 | ```bash 183 | export KEYSTORE_PASSWORD=sillyPassword 184 | export TRUSTSTORE_PASSWORD=sillyPassword 185 | ``` 186 | 187 | * generate required keys 188 | * copy in proxy certificate into same ssl folder and name it proxy_cert.pem 189 | 190 | ```bash 191 | cd src/main/resources/ssl 192 | ./generatepems.sh 193 | # must type in passwords matching the environment variables when prompted below 194 | ./generatekeystore.sh 195 | ./generatetrust.sh 196 | ./importkey.sh 197 | ``` 198 | 199 | ```bash 200 | redis-cli -u $REDIS_CONNECTION --tls --cacert src/main/resources/ssl/proxy_cert.pem --cert src/main/resources/ssl/client_cert_app_001.pem --key src/main/resources/ssl/client_key_app_001.pem -a $REDIS_PASSWORD 201 | ``` 202 | 203 | * Turn SSL on for the application. (Two different ways) in both ways, must set spring.redis.ssl to true 204 | * Can change src/main/resources/application.properties to add the key and trust store parameters 205 | 206 | ```bash 207 | spring.redis.ssl=true 208 | server.ssl.key-store=./src/main/resources/ssl/client-keystore.p12 209 | server.ssl.key-store-password=${KEYSTORE_PASSWORD} 210 | server.ssl.trust-store=./src/main/resources/ssl/client-truststore.p12 211 | server.ssl.trust-store-password=${TRUSTSTORE_PASSWORD} 212 | ``` 213 | 214 | or can change the runtime (sample script included) 215 | 216 | 217 | 218 | * package and run application 219 | 220 | ```bash 221 | mvn clean package 222 | java -jar target/redis-0.0.1-SNAPSHOT.jar 223 | ``` 224 | 225 | WARNING: This causes TLS to be turned on for the application which causes the following chages: 226 | * port changes from 8080 to 8443 227 | * must disable the failure on non-certified key in each of the scripts. This format works: 228 | 229 | ```bash 230 | curl --insecure -I 231 | ``` 232 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/data/BankGenerator.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.data; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.*; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | import com.jphaugla.domain.*; 8 | import org.joda.time.DateTime; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | public class BankGenerator { 13 | private static final Logger logger = LoggerFactory.getLogger(BankGenerator.class); 14 | 15 | // private static final Logger logger = LoggerFactory.getLogger(BankGenerator.class); 16 | private static final int BASE = 1000000; 17 | private static final int DAY_MILLIS = 1000 * 60 *60 * 24; 18 | private static AtomicInteger customerIdGenerator = new AtomicInteger(1); 19 | private static AtomicInteger accountNoGenerator = new AtomicInteger(1); 20 | private static List accountTypes = Arrays.asList("Current", "Joint Current", "Saving", "Mortgage", 21 | "E-Saving", "Deposit"); 22 | private static List accountIds = new ArrayList(); 23 | private static Map> accountsMap = new HashMap>(); 24 | 25 | //We can change this from the Main 26 | public static DateTime date = new DateTime().minusDays(180).withTimeAtStartOfDay(); 27 | public static Date currentDate = new Date(); 28 | public static String timeStamp = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss").format(currentDate); 29 | 30 | public static List whiteList = new ArrayList(); 31 | 32 | public static String getRandomCustomerId(int noOfCustomers){ 33 | return BASE + new Double(Math.random()*noOfCustomers).intValue() + ""; 34 | } 35 | 36 | public static List createEmail (String customerId) { 37 | Email home_email = new Email(customerId + "@gmail.com","home", customerId); 38 | Email work_email = new Email(customerId + "@BigCompany.com","work", customerId); 39 | List emailList; 40 | emailList = new ArrayList(); 41 | emailList.add(home_email); 42 | emailList.add(work_email); 43 | return emailList; 44 | } 45 | 46 | public static List createPhone (String customerId) { 47 | Phone home_phone = new Phone(customerId + "h", "home", customerId); 48 | Phone cell_phone = new Phone(customerId + "c", "cell", customerId); 49 | Phone workPhone = new Phone(customerId + "w", "work", customerId); 50 | List phoneList; 51 | phoneList = new ArrayList(); 52 | phoneList.add(home_phone); 53 | phoneList.add(cell_phone); 54 | phoneList.add(workPhone); 55 | return phoneList; 56 | } 57 | 58 | public static Customer createRandomCustomer(String key_suffix) { 59 | 60 | String customerIdInt = BASE + customerIdGenerator.getAndIncrement() + ""; 61 | String customerId = customerIdInt + key_suffix; 62 | 63 | Customer customer = new Customer(); 64 | customer.setCustomerId(customerId); 65 | 66 | customer.setAddressLine1("Line1-" + customerId); 67 | customer.setCreatedBy("Java Test"); 68 | customer.setLastUpdatedBy("Java Test"); 69 | customer.setCustomerType("Retail"); 70 | 71 | customer.setCreatedDatetime(currentDate.getTime()); 72 | customer.setLastUpdated(currentDate.getTime()); 73 | 74 | customer.setCustomerOriginSystem("RCIF"); 75 | customer.setCustomerStatus("A"); 76 | customer.setCountryCode("00"); 77 | customer.setGovernmentId("TIN"); 78 | customer.setGovernmentIdType(customerIdInt.substring(1)); 79 | 80 | int lastDigit = Integer.parseInt(customerIdInt.substring(6)); 81 | if (lastDigit>7) { 82 | customer.setAddressLine2("Apt " + customerId); 83 | customer.setAddressType("Apartment"); 84 | customer.setBillPayEnrolled("false"); 85 | } 86 | else if (lastDigit==3){ 87 | customer.setBillPayEnrolled("false"); 88 | customer.setAddressType("Mobile"); 89 | } 90 | else { 91 | customer.setAddressType("Residence"); 92 | customer.setBillPayEnrolled("true"); 93 | } 94 | customer.setCity(locations.get(lastDigit)); 95 | customer.setStateAbbreviation(States[lastDigit]); 96 | customer.setDateOfBirth(dob.get(lastDigit)); 97 | String lastName = customerId.substring(2,7); 98 | String firstName = firstList.get(lastDigit); 99 | String middleName = middleList.get(lastDigit); 100 | customer.setGender(genderList[lastDigit]); 101 | if (genderList[lastDigit]=="F"){ 102 | customer.setPrefix("Ms"); 103 | } 104 | else { 105 | customer.setPrefix("Mr"); 106 | } 107 | String zipChar = zipcodeList.get(lastDigit).toString(); 108 | customer.setZipcode(zipChar); 109 | customer.setZipcode4(zipChar + "-1234"); 110 | customer.setFirstName(firstName); 111 | customer.setLastName(lastName); 112 | customer.setMiddleName(middleName); 113 | customer.setFullName(firstName + " " + middleName + " " + lastName); 114 | 115 | 116 | return customer; 117 | } 118 | public static List createMerchantList () { 119 | List merchants = new ArrayList<>(); 120 | int iterations = issuers.length; 121 | for (int i=0; i < iterations; i++) { 122 | merchants.add(new Merchant(issuers[i], issuersCD[i], issuersCDdesc[i], States[i],"US")); 123 | } 124 | return merchants; 125 | }; 126 | public static List createMerchantName () { 127 | List merchants = new ArrayList<>(); 128 | // for(String merchant:issuers) { 129 | merchants = Arrays.asList(issuers); 130 | return merchants; 131 | } 132 | 133 | public static List createTransactionReturnList() { 134 | List transactionReturns = new ArrayList<>(); 135 | transactionReturns.add(new TransactionReturn("1345","incorrect amount recorded")); 136 | transactionReturns.add(new TransactionReturn("1554","not authorized transaction")); 137 | transactionReturns.add(new TransactionReturn("6555","wrong account")); 138 | return transactionReturns; 139 | }; 140 | 141 | public static List createRandomAccountsForCustomer(Customer customer, String key_suffix) { 142 | 143 | int noOfAccounts = Math.random() < .1 ? 4 : 3; 144 | List accounts = new ArrayList(); 145 | 146 | 147 | for (int i = 0; i < noOfAccounts; i++){ 148 | 149 | Account account = new Account(); 150 | String accountNumber = "Acct" + accountNoGenerator.getAndIncrement() + "" + key_suffix; 151 | // String accountNumber = "Acct" + Integer.toString(i) + key_suffix; 152 | account.setCardNum( UUID.randomUUID().toString().replace('-','x')); 153 | account.setCustomerId(customer.getCustomerId()); 154 | account.setAccountNo(accountNumber); 155 | account.setAccountType(accountTypes.get(i)); 156 | account.setAccountStatus("Open"); 157 | account.setLastUpdatedBy("Java Test"); 158 | account.setLastUpdated(currentDate.getTime()); 159 | account.setCreatedDatetime(currentDate.getTime()); 160 | account.setCreatedBy("Java Test"); 161 | 162 | 163 | accounts.add(account); 164 | 165 | //Keep a list of all Account Nos to create the transactions 166 | accountIds.add(account.getAccountNo()); 167 | } 168 | 169 | return accounts; 170 | } 171 | public static Transaction createRandomTransaction(int noOfDays, Integer idx, Account account, 172 | String key_suffix, List merchants, 173 | List transactionReturns) { 174 | 175 | int noOfMillis = noOfDays * DAY_MILLIS; 176 | // create time by adding a random no of millis 177 | DateTime newDate = date.plusMillis(new Double(Math.random() * noOfMillis).intValue() + 1); 178 | 179 | return createRandomTransaction(newDate, idx, account, key_suffix, merchants, transactionReturns); 180 | } 181 | public static Transaction createRandomTransaction(DateTime newDate, Integer idx, Account account, 182 | String key_suffix,List merchants, 183 | List transactionReturns) { 184 | 185 | String location = locations.get(new Double(Math.random() * locations.size()).intValue()); 186 | int noOfItems = new Double(Math.ceil(Math.random() * 3)).intValue(); 187 | double doubleRandomLocation = new Double(Math.random() * issuers.length); 188 | int randomLocation = (int) doubleRandomLocation; 189 | 190 | Date aNewDate = newDate.toDate(); 191 | Date oldDate = new Date(0); 192 | Calendar calendar = Calendar.getInstance(); 193 | calendar.setTime(aNewDate); 194 | calendar.add(Calendar.DATE, -1); 195 | Date date_minus_one =calendar.getTime(); 196 | calendar.add(Calendar.DATE, -1); 197 | Date date_minus_two = calendar.getTime(); 198 | 199 | Transaction transaction = new Transaction(); 200 | createItemsAndAmount(noOfItems, transaction); 201 | transaction.setAccountNo(account.getAccountNo()); 202 | // String tran_id = "{" + account.getAccountNo() + "}" + idx.toString() + key_suffix; 203 | String tran_id = idx.toString() + key_suffix; 204 | transaction.setTranId(tran_id); 205 | String transactionStat = transactionStatus[randomLocation]; 206 | transaction.setStatus(transactionStat); 207 | if(transactionStat == "POSTED") { 208 | transaction.setPostingDate(aNewDate.getTime()); 209 | transaction.setSettlementDate(date_minus_one.getTime()); 210 | transaction.setInitialDate(date_minus_two.getTime()); 211 | } else if (transactionStat == "SETTLED") { 212 | transaction.setSettlementDate(aNewDate.getTime()); 213 | transaction.setInitialDate(date_minus_one.getTime()); 214 | transaction.setPostingDate(oldDate.getTime()); 215 | } else { 216 | transaction.setInitialDate(aNewDate.getTime()); 217 | transaction.setPostingDate(oldDate.getTime()); 218 | transaction.setSettlementDate(oldDate.getTime()); 219 | } 220 | transaction.setLocation(location); 221 | if(randomLocation<5) { 222 | transaction.setAmountType("Debit"); 223 | } 224 | else{ 225 | transaction.setAmountType("Credit"); 226 | } 227 | transaction.setMerchant(merchants.get(randomLocation).getName()); 228 | 229 | transaction.setReferenceKeyType("reftype"); 230 | transaction.setReferenceKeyValue("thisRef"); 231 | 232 | transaction.setTranCd(issuersCD[randomLocation]); 233 | transaction.setDescription("description" + issuersCD[randomLocation] + genderList[randomLocation/3 + 1]); 234 | if(randomLocation==8) { 235 | transaction.setTransactionReturn(transactionReturns.get(0).getReasonCode()); 236 | } else if (randomLocation == 13) { 237 | transaction.setTransactionReturn(transactionReturns.get(1).getReasonCode()); 238 | } 239 | return transaction; 240 | } 241 | 242 | /** 243 | * Creates a random transaction with some skew for some accounts. 244 | * @return 245 | */ 246 | 247 | private static void createItemsAndAmount(int noOfItems, Transaction transaction) { 248 | Map items = new HashMap(); 249 | double totalAmount = 0; 250 | 251 | for (int i = 0; i < noOfItems; i++) { 252 | 253 | double amount = new Double(Math.random() * 100); 254 | items.put("item" + i, amount); 255 | 256 | totalAmount += amount; 257 | } 258 | transaction.setAmount(String.valueOf(totalAmount)); 259 | transaction.setOriginalAmount(String.valueOf(totalAmount)); 260 | } 261 | 262 | 263 | public static class Timer { 264 | 265 | private long timeTaken; 266 | private long start; 267 | 268 | public Timer(){ 269 | start(); 270 | } 271 | public void start(){ 272 | this.start = System.currentTimeMillis(); 273 | } 274 | public void end(){ 275 | this.timeTaken = System.currentTimeMillis() - start; 276 | } 277 | 278 | public long getTimeTakenMillis(){ 279 | return this.timeTaken; 280 | } 281 | 282 | public int getTimeTakenSeconds(){ 283 | return new Double(this.timeTaken / 1000).intValue(); 284 | } 285 | 286 | public String getTimeTakenMinutes(){ 287 | return String.format("%1$,.2f", new Double(this.timeTaken / (1000*60))); 288 | } 289 | 290 | } 291 | 292 | 293 | public static List locations = Arrays.asList("Chicago", "Minneapolis", "St. Paul", "Plymouth", "Edina", 294 | "Duluth", "Bloomington", "Bloomington", "Rockford", "Champaign"); 295 | public static List zipcodeList = Arrays.asList(60601, 55401, 55101, 55441, 55435, 296 | 55802, 61704, 55435, 61101, 16821); 297 | public static List dob = Arrays.asList("08/19/1964", "07/14/1984", "01/20/2000", "06/10/1951", "11/22/1995", 298 | "12/13/1954", "08/12/1943", "11/29/1964", "02/01/1994", "07/12/1944"); 299 | public static String[] transactionStatus = {"POSTED", "AUTHORIZED", "SETTLED", "POSTED", "POSTED", "POSTED", 300 | "POSTED", "POSTED", "POSTED", "POSTED", "POSTED", "POSTED", 301 | "POSTED", "POSTED", "POSTED", "POSTED", "POSTED", "POSTED", 302 | "AUTHORIZED", "SETTLED","POSTED","AUTHORIZED","SETTLED","POSTED","POSTED","POSTED"}; 303 | public static String[] States = {"IL", "MN", "MN", "MN", "MN","CA", "AZ", "AL", "AK", "TX", "WY", "PR", 304 | "MN", "IL", "MN", "MN", "IL", "IA", "WI", "SD", "ND", "MD", "CT", "WI", "KS", "IN","DE","TN" 305 | }; 306 | 307 | public static String[] genderList = {"M", "F", "F", "M", "F", "F", "M", "M", "M", "F"}; 308 | 309 | public static List middleList = Arrays.asList("Paul", "Ann", "Mary", "Joseph", "Amy", 310 | "Elizabeth", "John", "Javier", "Golov", "Eliza"); 311 | 312 | public static List firstList = Arrays.asList("Jason", "Catherine", "Esmeralda", "Marcus", "Louisa", 313 | "Julia", "Miles", "Luis", "Igor", "Angela"); 314 | 315 | public static String[] issuers = {"Tesco", "Sainsbury", "Wal-Mart Stores", "Morrisons", 316 | "Marks & Spencer", "Walmart", "John Lewis", "Cub Foods", "Argos", "Co-op", "Currys", "PC World", "B&Q", 317 | "Somerfield", "Next", "Spar", "Amazon", "Costa", "Starbucks", "BestBuy", "Lowes", "BarnesNoble", 318 | "Carlson Wagonlit Travel", "Pizza Hut", "Local Pub"}; 319 | 320 | public static String[] issuersCD = {"5411", "5411", "5310", "5499", 321 | "5310", "5912", "5311", "5411", "5961", "5300", "5732", "5964", "5719", 322 | "5411", "5651", "5411", "5310", "5691", "5814", "5732", "5211", "5942", "5962", 323 | "5814", "5813"}; 324 | 325 | public static String[] issuersCDdesc = {"Grocery Stores", "Grocery Stores", 326 | "Discount Stores", "Misc Food Stores Convenience Stores and Specialty Markets", 327 | "Discount Stores", "Drug Stores and Pharmacies", "Department Stores", "Supermarkets", "Mail Order Houses", 328 | "Wholesale Clubs", "Electronic Sales", 329 | "Direct Marketing Catalog Merchant", "Miscellaneous Home Furnishing Specialty Stores", 330 | "Grocery Stores", "Family Clothing Stores", "Grocery Stores", "Discount Stores", 331 | "Mens and Womens Clothing Stores", 332 | "Fast Food Restaurants", 333 | "Electronic Sales", 334 | "Lumber and Building Materials Stores", 335 | "Book Stores", "Direct Marketing Travel Related Services", 336 | "Fast Food Restaurants", "Drinking Places, Bars, Taverns, Cocktail lounges, Nightclubs and Discos"}; 337 | 338 | 339 | public static List notes = Arrays.asList("Shopping", "Shopping", "Shopping", "Shopping", "Shopping", 340 | "Pharmacy", "HouseHold", "Shopping", "Household", "Shopping", "Tech", "Tech", "Diy", "Shopping", "Clothes", 341 | "Shopping", "Amazon", "Coffee", "Coffee", "Tech", "Diy", "Travel", "Travel", "Eating out", "Eating out"); 342 | 343 | public static List tagList = Arrays.asList("Home", "Home", "Home", "Home", "Home", "Home", "Home", "Home", 344 | "Work", "Work", "Work", "Home", "Home", "Home", "Work", "Work", "Home", "Work", "Work", "Work", "Work", 345 | "Work", "Work", "Work", "Work", "Expenses", "Luxury", "Entertaining", "School"); 346 | 347 | } 348 | -------------------------------------------------------------------------------- /src/main/java/com/jphaugla/service/BankService.java: -------------------------------------------------------------------------------- 1 | package com.jphaugla.service; 2 | 3 | import java.text.ParseException; 4 | import java.text.SimpleDateFormat; 5 | import java.util.*; 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.concurrent.ExecutionException; 8 | import java.util.concurrent.atomic.AtomicLong; 9 | import java.time.Instant; 10 | 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | import com.jphaugla.data.BankGenerator; 13 | import com.jphaugla.domain.*; 14 | import com.jphaugla.repository.*; 15 | 16 | import com.redislabs.mesclun.search.*; 17 | import com.redislabs.mesclun.StatefulRedisModulesConnection; 18 | import com.redislabs.mesclun.search.aggregate.GroupBy; 19 | import com.redislabs.mesclun.search.aggregate.reducers.Count; 20 | 21 | import io.lettuce.core.RedisCommandExecutionException; 22 | import org.joda.time.DateTime; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.springframework.beans.factory.annotation.Autowired; 26 | import org.springframework.beans.factory.annotation.Value; 27 | 28 | import org.springframework.dao.DataAccessException; 29 | import org.springframework.data.redis.connection.RedisConnection; 30 | import org.springframework.data.redis.core.RedisCallback; 31 | import org.springframework.data.redis.core.StringRedisTemplate; 32 | 33 | import org.springframework.stereotype.Service; 34 | 35 | 36 | @Service 37 | 38 | public class BankService { 39 | 40 | private static BankService bankService = new BankService(); 41 | @Autowired 42 | private AsyncService asyncService; 43 | @Autowired 44 | private CustomerRepository customerRepository; 45 | @Autowired 46 | private AccountRepository accountRepository; 47 | @Autowired 48 | private PhoneRepository phoneRepository; 49 | @Autowired 50 | private EmailRepository emailRepository; 51 | @Autowired 52 | private MerchantRepository merchantRepository; 53 | @Autowired 54 | private TransactionReturnRepository transactionReturnRepository; 55 | @Autowired 56 | private TransactionRepository transactionRepository; 57 | @Autowired 58 | private StringRedisTemplate redisTemplate; 59 | @Autowired 60 | ObjectMapper objectMapper; 61 | 62 | @Autowired 63 | private StatefulRedisModulesConnection connection; 64 | 65 | @Value("${app.transactionSearchIndexName}") 66 | private String transactionSearchIndexName; 67 | @Value("${app.transactionReturnSearchIndexName}") 68 | private String transactionReturnSearchIndexName; 69 | @Value("${app.customerSearchIndexName}") 70 | private String customerSearchIndexName; 71 | @Value("${app.merchantSearchIndexName}") 72 | private String merchantSearchIndexName; 73 | @Value("${app.accountSearchIndexName}") 74 | private String accountSearchIndexName; 75 | private static final Logger logger = LoggerFactory.getLogger(BankService.class); 76 | 77 | private long timerSum = 0; 78 | private AtomicLong timerCount= new AtomicLong(); 79 | 80 | public static BankService getInstance(){ 81 | return bankService; 82 | } 83 | // 84 | // Customer 85 | // 86 | public Optional getCustomer(String customerId){ 87 | logger.info("in bankservice.getCustomer with ID " + customerId); 88 | Customer returnCustomer = customerRepository.get(customerId); 89 | logger.info("returned customer " + returnCustomer); 90 | return Optional.of(returnCustomer); 91 | } 92 | 93 | public void saveSampleCustomer() throws ParseException, RedisCommandExecutionException { 94 | Date create_date = new SimpleDateFormat("yyyy.MM.dd").parse("2020.03.28"); 95 | Date last_update = new SimpleDateFormat("yyyy.MM.dd").parse("2020.03.29"); 96 | String cust = "cust0001"; 97 | Email home_email = new Email("jasonhaugland@gmail.com", "home", cust); 98 | Email work_email = new Email("jason.haugland@redislabs.com", "work", cust); 99 | Phone cell_phone = new Phone("612-408-4394", "cell", cust); 100 | emailRepository.create(home_email); 101 | emailRepository.create(work_email); 102 | phoneRepository.create(cell_phone); 103 | Customer customer = new Customer( cust, "4744 17th av s", "", 104 | "Home", "N", "Minneapolis", "00", 105 | "jph", create_date.getTime(), "IDR", 106 | "A", "BANK", "1949.01.23", 107 | "Ralph", "Ralph Waldo Emerson", "M", 108 | "887778989", "SSN", "Emerson", last_update.getTime(), 109 | "jph", "Waldo", "MR", 110 | "help", "MN", "55444", "55444-3322" 111 | ); 112 | customerRepository.create(customer); 113 | } 114 | 115 | public void postCustomer(Customer customer) { 116 | logger.info("in postCustomer with Customer =" + customer); 117 | customerRepository.create(customer); 118 | Email home_email = new Email(customer.getCustomerId() + "@gmail.com", "home", customer.getCustomerId()); 119 | Email work_email = new Email(customer.getCustomerId() + "@redislabs.com", "work", customer.getCustomerId()); 120 | Phone cell_phone = new Phone("612-408-4394", "cell", customer.getCustomerId()); 121 | emailRepository.create(home_email); 122 | emailRepository.create(work_email); 123 | phoneRepository.create(cell_phone); 124 | } 125 | 126 | public int deleteCustomer(String customerString) { 127 | // List customerIDList = customerRepository.findByStateAbbreviationAndCity(state, city); 128 | RediSearchCommands commands = connection.sync(); 129 | String queryString = "@customerId:" + customerString; 130 | logger.info("query string is " + queryString); 131 | int returnValue = 0; 132 | SearchResults results = commands.search(customerSearchIndexName, queryString); 133 | returnValue = results.size(); 134 | for (Document document : results) { 135 | String fullKey = (String) document.getId(); 136 | redisTemplate.delete(fullKey); 137 | // logger.warn("adding to transaction list string=" + onlyID + " fullKey is " + fullKey) 138 | } 139 | return (returnValue); 140 | } 141 | 142 | public SearchResults getCustomerByStateCity(String state, String city){ 143 | 144 | // List customerIDList = customerRepository.findByStateAbbreviationAndCity(state, city); 145 | RediSearchCommands commands = connection.sync(); 146 | String queryString = "@stateAbbreviation:" + state + " @city:" + city; 147 | logger.info("query string is " + queryString); 148 | SearchResults results = commands.search(customerSearchIndexName, queryString); 149 | return results; 150 | } 151 | 152 | public SearchResults getCustomerIdsbyZipcodeLastname(String zipcode, String lastName){ 153 | RediSearchCommands commands = connection.sync(); 154 | String queryString = "@zipcode:" + zipcode + " @lastName:" + lastName; 155 | SearchResults results = commands.search(customerSearchIndexName, queryString); 156 | return results; 157 | } 158 | 159 | 160 | 161 | // 162 | // Phone 163 | // 164 | public Optional getPhoneNumber(String phoneString) { 165 | return phoneRepository.get(phoneString); 166 | } 167 | 168 | public Customer getCustomerByPhone(String phoneString) { 169 | // get list of customers having this phone number 170 | // first, get phone hash with this phone number 171 | // next, get the customer id with this phone number 172 | // third, use the customer id to get the customer 173 | Optional optPhone = getPhoneNumber(phoneString); 174 | Optional returnCustomer = null; 175 | Customer returnCust = null; 176 | logger.info("in bankservice.getCustomerByPhone optphone is" + optPhone.isPresent()); 177 | if (optPhone.isPresent()) { 178 | Phone onePhone = optPhone.get(); 179 | String customerId = onePhone.getCustomerId(); 180 | logger.info(" onePhone is " + onePhone.getPhoneNumber() + ":" + onePhone.getPhoneLabel() + ":" + onePhone.getCustomerId()); 181 | returnCustomer = Optional.ofNullable(customerRepository.get(customerId)); 182 | } 183 | 184 | if ((returnCustomer != null) && (returnCustomer.isPresent())) { 185 | returnCust = returnCustomer.get(); 186 | // logger.info("customer is " + returnCust); 187 | 188 | } else { 189 | returnCust = null; 190 | } 191 | return returnCust; 192 | } 193 | // 194 | // Email 195 | // 196 | public Optional getEmail(String email) { 197 | return Optional.ofNullable(emailRepository.get(email)); 198 | } 199 | 200 | public Customer getCustomerByEmail(String emailString) { 201 | // get list of customers having this email number 202 | // first, get email hash with this email number 203 | // next, get the customer id with this email number 204 | // third, use the customer id to get the customer 205 | Optional optionalEmail = getEmail(emailString); 206 | Optional returnCustomer = null; 207 | Customer returnCust = null; 208 | logger.info("in bankservice.getCustomerByEmail optEmail is" + optionalEmail.isPresent()); 209 | if (optionalEmail.isPresent()) { 210 | Email oneEmail = optionalEmail.get(); 211 | String customerId = oneEmail.getCustomerId(); 212 | // logger.info("customer is " + customerId); 213 | returnCustomer = Optional.ofNullable(customerRepository.get(customerId)); 214 | } 215 | 216 | if ((returnCustomer != null) && (returnCustomer.isPresent())) { 217 | returnCust = returnCustomer.get(); 218 | logger.info("customer is " + returnCust); 219 | 220 | } else { 221 | returnCust = null; 222 | } 223 | return returnCust; 224 | } 225 | 226 | public int deleteCustomerEmail(String customerID) { 227 | logger.info("in bankservice.deleteCustomerEmail with CustomerID " + customerID); 228 | int deleteCount = emailRepository.deleteCustomerEmails(customerID); 229 | return deleteCount; 230 | } 231 | 232 | // 233 | // Utility methods 234 | // 235 | private void sleep(int i) { 236 | try { 237 | Thread.sleep(i); 238 | } catch (InterruptedException e) { 239 | e.printStackTrace(); 240 | } 241 | } 242 | 243 | public String getDateFullDayQueryString(String stringDate) throws ParseException { 244 | Date inDate = new SimpleDateFormat("MM/dd/yyyy").parse(stringDate); 245 | Long inUnix = inDate.getTime(); 246 | // since the transaction ID is also in the query can take a larger reach around the date column 247 | Long startUnix = inUnix - 86400*1000; 248 | Long endUnix = inUnix + 86400*1000; 249 | return " @postingDate:[" + startUnix + " " + endUnix + "]"; 250 | } 251 | 252 | public String getDateToFromQueryString(Date startDate, Date endDate) throws ParseException { 253 | /* Date toDate = new SimpleDateFormat("MM/dd/yyyy").parse(to); 254 | Date fromDate = new SimpleDateFormat("MM/dd/yyyy").parse(from); 255 | */ 256 | Long startUnix = startDate.getTime(); 257 | Long endUnix = endDate.getTime(); 258 | return " @postingDate:[" + startUnix + " " + endUnix + "]"; 259 | } 260 | 261 | // 262 | // Transaction 263 | // 264 | 265 | public Transaction getTransaction(String transactionID) { 266 | Optional optionalTransaction; 267 | Transaction returnTransaction = null; 268 | optionalTransaction = Optional.ofNullable(transactionRepository.get(transactionID)); 269 | if(optionalTransaction.isPresent()) { 270 | returnTransaction = optionalTransaction.get(); 271 | } 272 | return returnTransaction; 273 | } 274 | private List getTransactionByStatus(String transactionStatus) throws ExecutionException, InterruptedException { 275 | RediSearchCommands commands = connection.sync(); 276 | String queryString = "@status:" + transactionStatus; 277 | SearchOptions searchOptions = SearchOptions.builder().limit(SearchOptions.Limit.offset(0).num(100000)).clearReturnFields().build(); 278 | SearchResults results = commands.search(transactionSearchIndexName, queryString, searchOptions); 279 | // this code snippet get converts results to List of Transaction IDs 280 | List transIdList = new ArrayList(); 281 | for (Document document : results) { 282 | String fullKey = (String) document.getId(); 283 | String onlyID = fullKey.replace(transactionSearchIndexName + ':', ""); 284 | transIdList.add(onlyID); 285 | // logger.warn("adding to transaction list string=" + onlyID + " fullKey is " + fullKey); 286 | } 287 | return transIdList; 288 | } 289 | public void transactionStatusChange(String targetStatus) throws IllegalAccessException, ExecutionException, InterruptedException { 290 | // move target from authorized->settled->posted 291 | logger.info("transactionStatusChange targetStatus is " + targetStatus); 292 | SearchResults documents; 293 | CompletableFuture transaction_cntr = null; 294 | List transIdList = new ArrayList(); 295 | long unixTime = Instant.now().getEpochSecond(); 296 | String stringUnixTime=String.valueOf(unixTime); 297 | if(targetStatus.equals("POSTED")) { 298 | transIdList = getTransactionByStatus("SETTLED"); 299 | } else { 300 | transIdList = getTransactionByStatus("AUTHORIZED"); 301 | } 302 | logger.info("number of transactions " + transIdList.size()); 303 | 304 | for(String tranID: transIdList) { 305 | redisTemplate.opsForHash().put(transactionSearchIndexName + ':' + tranID,"status", targetStatus); 306 | if(targetStatus.equals("POSTED")) { 307 | redisTemplate.opsForHash().put(transactionSearchIndexName + ':' + tranID, "postingDate", stringUnixTime); 308 | } else { 309 | redisTemplate.opsForHash().put(transactionSearchIndexName + ':' + tranID, "settlementDate", stringUnixTime); 310 | } 311 | } 312 | logger.info("Finished updating " + transIdList.size()); 313 | } 314 | 315 | // writeTransaction using crud without future 316 | private void writeTransaction(Transaction transaction) { 317 | // logger.info("writing a transaction " + transaction); 318 | transactionRepository.create(transaction); 319 | } 320 | // writeTransaction using crud with Future 321 | private CompletableFuture writeTransactionFuture(Transaction transaction) throws IllegalAccessException { 322 | CompletableFuture transaction_cntr = null; 323 | transaction_cntr = asyncService.writeTransaction(transaction); 324 | // writes a sorted set to be used as the posted date index 325 | 326 | return transaction_cntr; 327 | } 328 | public AggregateResults transactionStatusReport() { 329 | RediSearchCommands commands = connection.sync(); 330 | AggregateResults aggregateResults = commands.aggregate(transactionSearchIndexName, "*", 331 | AggregateOptions.builder().load("status").operation(GroupBy.properties("status").reducer(Count.as("COUNT")).build()).build()); 332 | return (aggregateResults); 333 | } 334 | public void saveSampleTransaction() throws ParseException, RedisCommandExecutionException { 335 | Date settle_date = new SimpleDateFormat("yyyy.MM.dd").parse("2021.07.28"); 336 | Date post_date = new SimpleDateFormat("yyyy.MM.dd").parse("2021.07.28"); 337 | Date init_date = new SimpleDateFormat("yyyy.MM.dd").parse("2021.07.27"); 338 | 339 | Merchant merchant = new Merchant("Cub Foods", "5411", 340 | "Grocery Stores", "MN", "US"); 341 | logger.info("before save merchant"); 342 | merchantRepository.create(merchant); 343 | 344 | Transaction transaction = new Transaction("1234", "acct01", 345 | "Debit", merchant.getName() + ":" + "acct01", "referenceKeyType", 346 | "referenceKeyValue", "323.23", "323.22", "1631", 347 | "Test Transaction", init_date.getTime(), settle_date.getTime(), post_date.getTime(), 348 | "POSTED", null, "ATM665", "Outdoor"); 349 | logger.info("before save transaction"); 350 | writeTransaction(transaction); 351 | } 352 | 353 | public void addTag(String transactionID, String tag, String operation) { 354 | logger.info("in addTag with transID=" + transactionID + " and tag " + tag); 355 | // hold set of transactions for a tag on an account 356 | String transactionKey = transactionSearchIndexName + ":" + transactionID; 357 | HashSet tagHash = getTransactionTagList(transactionID); 358 | 359 | if(operation.equals("ADD")) { 360 | tagHash.add(tag); 361 | } 362 | else { 363 | tagHash.remove(tag); 364 | } 365 | String tagDelimitedString = String.join(":", tagHash); 366 | redisTemplate.opsForHash().put(transactionKey,"transactionTags",tagDelimitedString); 367 | } 368 | 369 | public HashSet getTransactionTagList(String transactionID) { 370 | logger.info("in getTransactionTagList with transactionID=" + transactionID); 371 | // hold set of transactions for a tag on an account 372 | String transactionKey = transactionSearchIndexName + ":" + transactionID; 373 | String existingTags = (String) redisTemplate.opsForHash().get(transactionKey,"transactionTags"); 374 | HashSet tagHash = new HashSet(); 375 | if (existingTags != null) { 376 | String[] tagArray = existingTags.split(":"); 377 | List tagList = Arrays.asList(tagArray); 378 | tagHash = new HashSet(tagList); 379 | } 380 | return tagHash; 381 | } 382 | 383 | public SearchResults getTaggedTransactions(String accountNo, String tag) { 384 | logger.info("in getTaggedTransactions with accountNo=" + accountNo + " and tag " + tag); 385 | RediSearchCommands commands = connection.sync(); 386 | String queryString = "@accountNo:" + accountNo + " @transactionTags:{" + tag + "}"; 387 | logger.info("query is " + queryString); 388 | SearchResults accountResults = commands.search(transactionSearchIndexName, queryString); 389 | 390 | return accountResults; 391 | } 392 | 393 | public String testPipeline(Integer noOfRecords) { 394 | BankGenerator.Timer pipelineTimer = new BankGenerator.Timer(); 395 | this.redisTemplate.executePipelined(new RedisCallback() { 396 | @Override 397 | public Object doInRedis(RedisConnection connection) 398 | throws DataAccessException { 399 | connection.openPipeline(); 400 | String keyAndValue=null; 401 | for (int index=0;index accounts = createCustomerAccount(noOfCustomers, key_suffix); 420 | BankGenerator.date = new DateTime().minusDays(noOfDays).withTimeAtStartOfDay(); 421 | BankGenerator.Timer transTimer = new BankGenerator.Timer(); 422 | 423 | int totalTransactions = noOfTransactions * noOfDays; 424 | 425 | logger.info("Writing " + totalTransactions + " transactions for " + noOfCustomers 426 | + " customers. suffix is " + key_suffix + " Pipelined is " + pipelined); 427 | int account_size = accounts.size(); 428 | int transactionsPerAccount = noOfDays*noOfTransactions/account_size; 429 | logger.info("number of accounts generated is " + account_size + " transactionsPerAccount " 430 | + transactionsPerAccount); 431 | List merchants = BankGenerator.createMerchantList(); 432 | List transactionReturns = BankGenerator.createTransactionReturnList(); 433 | merchantRepository.createAll(merchants); 434 | transactionReturnRepository.createAll(transactionReturns); 435 | CompletableFuture transaction_cntr = null; 436 | if(pipelined) { 437 | logger.info("doing this pipelined"); 438 | int transactionIndex = 0; 439 | List transactionList = new ArrayList<>(); 440 | for(Account account:accounts) { 441 | for(int i=0; i getAccountTransactions(String account, Date startDate, Date endDate) 480 | throws ParseException, RedisCommandExecutionException { 481 | logger.info("in getAccountTransactions account is " + account); 482 | logger.info("startdate is " + startDate + " endDate is" + endDate); 483 | RediSearchCommands commands = connection.sync(); 484 | String tofromQuery = getDateToFromQueryString(startDate, endDate); 485 | String queryString = "@accountNo:" + account + tofromQuery; 486 | logger.info("query is " + queryString); 487 | SearchResults accountResults = commands.search(transactionSearchIndexName, queryString); 488 | 489 | return accountResults; 490 | }; 491 | public CompletableFuture writeAccountTransactions (List transactionList) throws IllegalAccessException, ExecutionException, InterruptedException { 492 | 493 | CompletableFuture returnVal = null; 494 | returnVal = asyncService.writeAccountTransactions(transactionList); 495 | return returnVal; 496 | } 497 | public SearchResults getCreditCardTransactions(String creditCard, Date startDate, Date endDate) 498 | throws ParseException, RedisCommandExecutionException { 499 | logger.info("credit card is " + creditCard + " start is " + startDate + " end is " + endDate); 500 | RediSearchCommands commands = connection.sync(); 501 | SearchResults transactionResults = null; 502 | String queryString = "@cardNum:" + creditCard; 503 | SearchResults accountResults = commands.search(accountSearchIndexName, queryString); 504 | // result set has all accounts with a credit card 505 | // build a query to match any of these merchants 506 | if(accountResults.getCount() > 0) { 507 | int i = 0; 508 | String accountListQueryString = "@accountNo:("; 509 | for (Document document : accountResults) { 510 | if (i > 0) accountListQueryString = accountListQueryString + "|"; 511 | String merchantKey = (String) document.getId(); 512 | String onlyID = merchantKey.replace(accountSearchIndexName + ':', ""); 513 | accountListQueryString = accountListQueryString + onlyID; 514 | i += 1; 515 | } 516 | accountListQueryString = accountListQueryString + ')'; 517 | logger.info("accountListQueryString is " + accountListQueryString); 518 | String tofromQuery = getDateToFromQueryString(startDate, endDate); 519 | queryString = accountListQueryString + tofromQuery; 520 | logger.info("queryString is " + queryString); 521 | transactionResults = commands.search(transactionSearchIndexName, queryString); 522 | } 523 | return transactionResults; 524 | }; 525 | private List createCustomerAccount(int noOfCustomers, String key_suffix) throws ExecutionException, InterruptedException, RedisCommandExecutionException { 526 | 527 | logger.info("Creating " + noOfCustomers + " customers with accounts and suffix " + key_suffix); 528 | BankGenerator.Timer custTimer = new BankGenerator.Timer(); 529 | List accounts = null; 530 | List allAccounts = new ArrayList<>(); 531 | List emails = null; 532 | List phoneNumbers = null; 533 | CompletableFuture account_cntr = null; 534 | CompletableFuture customer_cntr = null; 535 | CompletableFuture email_cntr = null; 536 | CompletableFuture phone_cntr = null; 537 | int totalAccounts = 0; 538 | int totalEmails = 0; 539 | int totalPhone = 0; 540 | logger.info("before the big for loop"); 541 | for (int i=0; i < noOfCustomers; i++){ 542 | logger.info("int noOfCustomer for loop i=" + i); 543 | Customer customer = BankGenerator.createRandomCustomer(key_suffix); 544 | List emailList = BankGenerator.createEmail(customer.getCustomerId()); 545 | List phoneList = BankGenerator.createPhone(customer.getCustomerId()); 546 | for (Phone phoneNumber : phoneNumbers = phoneList) { 547 | phone_cntr = asyncService.writePhone(phoneNumber); 548 | } 549 | totalPhone = totalPhone + phoneNumbers.size(); 550 | for (Email email: emails = emailList) { 551 | email_cntr = asyncService.writeEmail(email); 552 | } 553 | totalEmails = totalEmails + emails.size(); 554 | accounts = BankGenerator.createRandomAccountsForCustomer(customer, key_suffix); 555 | totalAccounts = totalAccounts + accounts.size(); 556 | for (Account account: accounts) { 557 | account_cntr = asyncService.writeAccounts(account); 558 | } 559 | customer_cntr = asyncService.writeCustomer(customer); 560 | if(accounts.size()>0) { 561 | allAccounts.addAll(accounts); 562 | } 563 | } 564 | logger.info("before the gets"); 565 | account_cntr.get(); 566 | customer_cntr.get(); 567 | email_cntr.get(); 568 | phone_cntr.get(); 569 | custTimer.end(); 570 | logger.info("Customers=" + noOfCustomers + " Accounts=" + totalAccounts + 571 | " Emails=" + totalEmails + " Phones=" + totalPhone + " in " + 572 | custTimer.getTimeTakenSeconds() + " secs"); 573 | return allAccounts; 574 | } 575 | 576 | // 577 | // TransactionReturns 578 | // 579 | 580 | public SearchResults getTransactionReturns() { 581 | logger.info("in getTransactionReturns "); 582 | RediSearchCommands commands = connection.sync(); 583 | String queryString = "*"; 584 | SearchResults merchantResults = commands.search(transactionReturnSearchIndexName, queryString); 585 | 586 | return merchantResults; 587 | } 588 | // 589 | // Merchant 590 | // 591 | public SearchResults getMerchantCategoryTransactions(String in_merchantCategory, String account, 592 | Date startDate, Date endDate) throws ParseException, RedisCommandExecutionException { 593 | RediSearchCommands commands = connection.sync(); 594 | String queryString = "@categoryCode:" + in_merchantCategory; 595 | SearchResults merchantResults = commands.search(merchantSearchIndexName, queryString); 596 | SearchResults transactionResults = null; 597 | // result set has all merchants with a category code 598 | // build a query to match any of these merchants 599 | if(merchantResults.getCount() > 0) { 600 | int i = 0; 601 | String merchantListQueryString = "@merchant:("; 602 | for (Document document : merchantResults) { 603 | if (i > 0) merchantListQueryString = merchantListQueryString + "|"; 604 | String merchantKey = (String) document.getId(); 605 | String onlyID = merchantKey.replace(merchantSearchIndexName + ':', ""); 606 | merchantListQueryString = merchantListQueryString + onlyID; 607 | i += 1; 608 | } 609 | merchantListQueryString = merchantListQueryString + ')'; 610 | logger.info("merchantListQueryString is " + merchantListQueryString); 611 | String tofromQuery = getDateToFromQueryString(startDate, endDate); 612 | queryString = "@accountNo:" + account + " " + merchantListQueryString + tofromQuery; 613 | logger.info("queryString is " + queryString); 614 | transactionResults = commands.search(transactionSearchIndexName, queryString); 615 | } 616 | return transactionResults; 617 | } 618 | 619 | public SearchResults getMerchantTransactions (String in_merchant, String account, Date startDate, Date endDate) 620 | throws ParseException, RedisCommandExecutionException { 621 | logger.info("in getMerchantTransactions merchant is " + in_merchant + " and account is " + account); 622 | logger.info("startdate is " + startDate + " endDate is" + endDate); 623 | RediSearchCommands commands = connection.sync(); 624 | String tofromQuery = getDateToFromQueryString(startDate, endDate); 625 | String queryString = "@merchant:" + in_merchant + " @accountNo:" + account + tofromQuery; 626 | logger.info("query is " + queryString); 627 | SearchResults merchantResults = commands.search(transactionSearchIndexName, queryString); 628 | 629 | return merchantResults; 630 | }; 631 | 632 | 633 | 634 | 635 | } 636 | --------------------------------------------------------------------------------