Repository: sqshq/piggymetrics Branch: master Commit: 6bb2cf9ddbca Files: 166 Total size: 455.9 KB Directory structure: gitextract_15q1n21c/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ └── bug-report-or-feature-request.md ├── .gitignore ├── .travis.yml ├── LICENCE ├── README.md ├── account-service/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── piggymetrics/ │ │ │ └── account/ │ │ │ ├── AccountApplication.java │ │ │ ├── client/ │ │ │ │ ├── AuthServiceClient.java │ │ │ │ ├── StatisticsServiceClient.java │ │ │ │ └── StatisticsServiceClientFallback.java │ │ │ ├── config/ │ │ │ │ └── ResourceServerConfig.java │ │ │ ├── controller/ │ │ │ │ ├── AccountController.java │ │ │ │ └── ErrorHandler.java │ │ │ ├── domain/ │ │ │ │ ├── Account.java │ │ │ │ ├── Currency.java │ │ │ │ ├── Item.java │ │ │ │ ├── Saving.java │ │ │ │ ├── TimePeriod.java │ │ │ │ └── User.java │ │ │ ├── repository/ │ │ │ │ └── AccountRepository.java │ │ │ └── service/ │ │ │ ├── AccountService.java │ │ │ ├── AccountServiceImpl.java │ │ │ └── security/ │ │ │ └── CustomUserInfoTokenServices.java │ │ └── resources/ │ │ └── bootstrap.yml │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── piggymetrics/ │ │ └── account/ │ │ ├── AccountServiceApplicationTests.java │ │ ├── client/ │ │ │ └── StatisticsServiceClientFallbackTest.java │ │ ├── controller/ │ │ │ └── AccountControllerTest.java │ │ ├── repository/ │ │ │ └── AccountRepositoryTest.java │ │ └── service/ │ │ └── AccountServiceTest.java │ └── resources/ │ ├── application.yml │ └── bootstrap.yml ├── auth-service/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── piggymetrics/ │ │ │ └── auth/ │ │ │ ├── AuthApplication.java │ │ │ ├── config/ │ │ │ │ ├── OAuth2AuthorizationConfig.java │ │ │ │ └── WebSecurityConfig.java │ │ │ ├── controller/ │ │ │ │ └── UserController.java │ │ │ ├── domain/ │ │ │ │ └── User.java │ │ │ ├── repository/ │ │ │ │ └── UserRepository.java │ │ │ └── service/ │ │ │ ├── UserService.java │ │ │ ├── UserServiceImpl.java │ │ │ └── security/ │ │ │ └── MongoUserDetailsService.java │ │ └── resources/ │ │ └── bootstrap.yml │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── piggymetrics/ │ │ └── auth/ │ │ ├── AuthServiceApplicationTests.java │ │ ├── controller/ │ │ │ └── UserControllerTest.java │ │ ├── repository/ │ │ │ └── UserRepositoryTest.java │ │ └── service/ │ │ ├── UserServiceTest.java │ │ └── security/ │ │ └── MongoUserDetailsServiceTest.java │ └── resources/ │ ├── application.yml │ └── bootstrap.yml ├── config/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── piggymetrics/ │ │ └── config/ │ │ ├── ConfigApplication.java │ │ └── SecurityConfig.java │ └── resources/ │ ├── application.yml │ └── shared/ │ ├── account-service.yml │ ├── application.yml │ ├── auth-service.yml │ ├── gateway.yml │ ├── monitoring.yml │ ├── notification-service.yml │ ├── registry.yml │ ├── statistics-service.yml │ └── turbine-stream-service.yml ├── docker-compose.dev.yml ├── docker-compose.yml ├── gateway/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── piggymetrics/ │ │ │ └── gateway/ │ │ │ └── GatewayApplication.java │ │ └── resources/ │ │ ├── bootstrap.yml │ │ └── static/ │ │ ├── attribution.html │ │ ├── css/ │ │ │ ├── animation.css │ │ │ ├── launch.css │ │ │ └── style.css │ │ ├── index.html │ │ └── js/ │ │ ├── dashboard.js │ │ ├── launch.js │ │ ├── lib/ │ │ │ ├── extrascripts.js │ │ │ └── touchscreens.js │ │ ├── login.js │ │ └── main.js │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── piggymetrics/ │ │ └── gateway/ │ │ └── GatewayApplicationTests.java │ └── resources/ │ └── bootstrap.yml ├── mongodb/ │ ├── Dockerfile │ ├── dump/ │ │ └── account-service-dump.js │ └── init.sh ├── monitoring/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── piggymetrics/ │ │ │ └── monitoring/ │ │ │ └── MonitoringApplication.java │ │ └── resources/ │ │ └── bootstrap.yml │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── piggymetrics/ │ │ └── monitoring/ │ │ └── MonitoringApplicationTests.java │ └── resources/ │ └── bootstrap.yml ├── notification-service/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── piggymetrics/ │ │ │ └── notification/ │ │ │ ├── NotificationServiceApplication.java │ │ │ ├── client/ │ │ │ │ └── AccountServiceClient.java │ │ │ ├── config/ │ │ │ │ └── ResourceServerConfig.java │ │ │ ├── controller/ │ │ │ │ └── RecipientController.java │ │ │ ├── domain/ │ │ │ │ ├── Frequency.java │ │ │ │ ├── NotificationSettings.java │ │ │ │ ├── NotificationType.java │ │ │ │ └── Recipient.java │ │ │ ├── repository/ │ │ │ │ ├── RecipientRepository.java │ │ │ │ └── converter/ │ │ │ │ ├── FrequencyReaderConverter.java │ │ │ │ └── FrequencyWriterConverter.java │ │ │ └── service/ │ │ │ ├── EmailService.java │ │ │ ├── EmailServiceImpl.java │ │ │ ├── NotificationService.java │ │ │ ├── NotificationServiceImpl.java │ │ │ ├── RecipientService.java │ │ │ └── RecipientServiceImpl.java │ │ └── resources/ │ │ └── bootstrap.yml │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── piggymetrics/ │ │ └── notification/ │ │ ├── NotificationServiceApplicationTests.java │ │ ├── controller/ │ │ │ └── RecipientControllerTest.java │ │ ├── repository/ │ │ │ └── RecipientRepositoryTest.java │ │ └── service/ │ │ ├── EmailServiceImplTest.java │ │ ├── NotificationServiceImplTest.java │ │ └── RecipientServiceImplTest.java │ └── resources/ │ ├── application.yml │ └── bootstrap.yml ├── pom.xml ├── registry/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── piggymetrics/ │ │ └── registry/ │ │ └── RegistryApplication.java │ └── resources/ │ └── bootstrap.yml ├── statistics-service/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── piggymetrics/ │ │ │ └── statistics/ │ │ │ ├── StatisticsApplication.java │ │ │ ├── client/ │ │ │ │ ├── ExchangeRatesClient.java │ │ │ │ └── ExchangeRatesClientFallback.java │ │ │ ├── config/ │ │ │ │ └── ResourceServerConfig.java │ │ │ ├── controller/ │ │ │ │ └── StatisticsController.java │ │ │ ├── domain/ │ │ │ │ ├── Account.java │ │ │ │ ├── Currency.java │ │ │ │ ├── ExchangeRatesContainer.java │ │ │ │ ├── Item.java │ │ │ │ ├── Saving.java │ │ │ │ ├── TimePeriod.java │ │ │ │ └── timeseries/ │ │ │ │ ├── DataPoint.java │ │ │ │ ├── DataPointId.java │ │ │ │ ├── ItemMetric.java │ │ │ │ └── StatisticMetric.java │ │ │ ├── repository/ │ │ │ │ ├── DataPointRepository.java │ │ │ │ └── converter/ │ │ │ │ ├── DataPointIdReaderConverter.java │ │ │ │ └── DataPointIdWriterConverter.java │ │ │ └── service/ │ │ │ ├── ExchangeRatesService.java │ │ │ ├── ExchangeRatesServiceImpl.java │ │ │ ├── StatisticsService.java │ │ │ ├── StatisticsServiceImpl.java │ │ │ └── security/ │ │ │ └── CustomUserInfoTokenServices.java │ │ └── resources/ │ │ └── bootstrap.yml │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── piggymetrics/ │ │ └── statistics/ │ │ ├── StatisticsServiceApplicationTests.java │ │ ├── client/ │ │ │ └── ExchangeRatesClientTest.java │ │ ├── controller/ │ │ │ └── StatisticsControllerTest.java │ │ ├── repository/ │ │ │ └── DataPointRepositoryTest.java │ │ └── service/ │ │ ├── ExchangeRatesServiceImplTest.java │ │ └── StatisticsServiceImplTest.java │ └── resources/ │ ├── application.yml │ └── bootstrap.yml └── turbine-stream-service/ ├── Dockerfile ├── pom.xml └── src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── piggymetrics/ │ │ └── turbine/ │ │ └── TurbineStreamServiceApplication.java │ └── resources/ │ └── bootstrap.yml └── test/ ├── java/ │ └── com/ │ └── piggymetrics/ │ └── turbine/ │ └── TurbineStreamServiceApplicationTests.java └── resources/ └── bootstrap.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report-or-feature-request.md ================================================ --- name: Bug report or feature request about: Suggest an idea or report a bug --- - If you have troubles to run the components, or you'd like to ask a question - please use [PiggyMetrics gitter chat](https://gitter.im/sqshq/PiggyMetrics) - If you'd like to propose an improvement or report a bug, please provide a clear and concise description, attach logs and screenshots if possible. The issue should be ready to be implemented without any additional questions. PiggyMetrics is open source, nobody maintains it during the work hours. If you'd like the issue to be fixed, please consider to contibute your time to do that. Thank you. ================================================ FILE: .gitignore ================================================ # Intellij .idea/ *.iml *.iws # Mac .DS_Store # Maven log/ target/ # Eclipse **/*.project **/*.classpath **/*.settings ================================================ FILE: .travis.yml ================================================ dist: trusty sudo: required services: - docker language: java jdk: oraclejdk8 env: global: - secure: "GeONVsTD48Y88CKoqupo/FC1Gy0eCrT1UNylvMzz5VYcLhWcUcu/d4870ZJznBqslGHbFaRc6VCXFLnbKNyR5Chgg127ouWOv/NFubJ5cO0UjHlBwoAxQ/SIrxG9W4B6Y2VeWRO0SIPHF6wUVeELiaPmIIe/jF15/SWJA926G2dF0+zTbn1KNWoVRi9pxZ0bIbEvVcVfHjWHsPtho916KRZ7ToV27f5E9DPLVMraK2HROOplJlXKNLuUor9xSmtOB/787yNrZmdsUaQDMHVfru9yEubfg4tF5ydoluB+JPzeUqIK2PrDgAiwmkEdoJFfbAOqwdy8vZkgpYd/t5vkhbOn3yA0QO1pnyhidx1YgyoJN3HwfMeVNQwUVg8jw7bltVe/DxsNo/AW11Tu7jKteuxewRHCxORzaUSmy5rnLq3AKIXT6h8Kq5VxP5tGbQypa7/z+Zu4vfOBdKGMiLllQ0vhhU0osgYzk/jT4KknuWQtOkX+QTS9iViIUwQgcn3kIMlz4eO83Mbt+IkNvgjF0DyE64mfV2ThTjXDV6959g3Nl/Fl95VMSTg95xtl0tbf733Lj+9HCIfLetlBz4ZzOqaDgD5fRL5HA8jR3FFHVdMd+dx/JELjSRHxO6ZFrCVG/2hBldIakO31/FbpODnDIkKO+86Oqz9DPKVnLJlmZp4=" # DOCKER_EMAIL - secure: "Gl6a03cI88dKHV4rjP1IkYqCdVe7IM0XNcEzFDCxmvXHWSpqlotntOYtgvtRKsYOzb0cfdQwgFuwj80glUbFX5vbX1y5r+qR5sQSrJVi61e8Pijn0MT7rE/d2gCJwaRTkR2lvtRRTIX41LA+aZ6F7+xFSd+ni82IlaXtDywQJWpCEmxcSqSUd5nVhHzH5JZmkYQmQ8fjxzGUhpeePfapYThPxXsHGxmJeoIlEDM1TEFtxVf3Zo9D10812uesa7EwNSKL6MOBU/Me7+liIHdRRpRwVmOjaFAZLeHsUfZQWcLWer0ODULov1U/YMdF860gV1X8PPYxAYNnWqevOGZIsYTX60yem/dCq90Lx7cIiK/TBZIS7x9k65QwP/shnO/RK8PPANBt8bJ4FBxmDPRMPRvgCPp4wQIYTyaiXd0M6BmK+LysS5cRgOOg7YF+ZRqKjNGY9rkNeGs0x8LIaE4Vz138tJCVJ0U+r2OOZFLCIu4dmvuOo1rWf4Hzh+Xt/nx8RrAwwqHKWRyayHkZEbQv2dNGcJsZyAzXsxA6NTDGQfKYloX5oY4qq3BMCkRoid9sLHoJvZrfkN6mjbZkEd2Ed3RzyTIxasmRUeD45vfr7go9ts1a6ppDrRYJWLi26pqyPhimTI0ljEYXp4QnK5JMya2y6H0wo/hJmzrqfXj4+C0=" # DOCKER_USER - secure: "VRlJyPOz7fUmtFdpTdO51BVcjKUGP5t7KF5bG7TSJPXsal7SSxjt6desLQ6zv31tKBp/SrgbnH4hloeAe38hL1I5gQKLPuNjOYhtolgLHlCO3aJiJ0vn0o6mgxOok4S/ul6EMy8VLbKzwt7GrruoaDNVadwc98NY+Grr1eLUzD5CVxa4luSahtKASheCtM29OQOj42Ivnc/MUUMYMymYa/zgIkROqI1ZbQK12NGwx13FzjjF2C9WyTR8IMFOeiuL/Ha8wxT2VeYExWkNw8TQKg35WT26axSoI92BBjPHY+l4IDQ0g8N176sAG52gbz4WXx60kRgqHrn+b5cjO/v1PknqXCqwWVrskm/mgGxJ4IVmpqa7ItDYtoVPzW4hyPsEHWyPMjii5280VdqVRueHyxSBH/uF3iQCDwR0hRdVnPPvgrt40/jbiD3pBhnDQErHCu54FA7uzfFT8LUvj3PgHn19KWAb4gKMpP0AZA3aDOD/3+db1x25ozhDXoCLPFk+kK0lp5mwTKka910lX4L25wp2P3RDdbGQTeVjBBp5+IxzfBuF7m2aEww7HgpB/TmHGaxo61cZfLFwcU2tfIQnPVRiomMqh85xFHIkDETNJJMdV1o+Im5maOzg3u5iy7E6dJec6jTYSCIzh6BemM+scYVZzLkYZVRyrUQkUj7ldoI=" # DOCKER_PASS - COMMIT=${TRAVIS_COMMIT::7} after_success: - bash <(curl -s https://codecov.io/bash) - docker login -u $DOCKER_USER -p $DOCKER_PASS #TAG - export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi` # CONFIG SERVICE - export CONFIG=sqshq/piggymetrics-config - docker build -t $CONFIG:$COMMIT ./config - docker tag $CONFIG:$COMMIT $CONFIG:$TAG - docker push $CONFIG # REGISTRY - export REGISTRY=sqshq/piggymetrics-registry - docker build -t $REGISTRY:$COMMIT ./registry - docker tag $REGISTRY:$COMMIT $REGISTRY:$TAG - docker push $REGISTRY # GATEWAY - export GATEWAY=sqshq/piggymetrics-gateway - docker build -t $GATEWAY:$COMMIT ./gateway - docker tag $GATEWAY:$COMMIT $GATEWAY:$TAG - docker push $GATEWAY # AUTH SERVICE - export AUTH_SERVICE=sqshq/piggymetrics-auth-service - docker build -t $AUTH_SERVICE:$COMMIT ./auth-service - docker tag $AUTH_SERVICE:$COMMIT $AUTH_SERVICE:$TAG - docker push $AUTH_SERVICE # ACCOUNT SERVICE - export ACCOUNT_SERVICE=sqshq/piggymetrics-account-service - docker build -t $ACCOUNT_SERVICE:$COMMIT ./account-service - docker tag $ACCOUNT_SERVICE:$COMMIT $ACCOUNT_SERVICE:$TAG - docker push $ACCOUNT_SERVICE # STATISTICS SERVICE - export STATISTICS_SERVICE=sqshq/piggymetrics-statistics-service - docker build -t $STATISTICS_SERVICE:$COMMIT ./statistics-service - docker tag $STATISTICS_SERVICE:$COMMIT $STATISTICS_SERVICE:$TAG - docker push $STATISTICS_SERVICE # NOTIFICATION_SERVICE - export NOTIFICATION_SERVICE=sqshq/piggymetrics-notification-service - docker build -t $NOTIFICATION_SERVICE:$COMMIT ./notification-service - docker tag $NOTIFICATION_SERVICE:$COMMIT $NOTIFICATION_SERVICE:$TAG - docker push $NOTIFICATION_SERVICE # MONITORING - export MONITORING=sqshq/piggymetrics-monitoring - docker build -t $MONITORING:$COMMIT ./monitoring - docker tag $MONITORING:$COMMIT $MONITORING:$TAG - docker push $MONITORING # TURBINE STREAM SERVICE - export TURBINE=sqshq/piggymetrics-turbine-stream-service - docker build -t $TURBINE:$COMMIT ./turbine-stream-service - docker tag $TURBINE:$COMMIT $TURBINE:$TAG - docker push $TURBINE # MONGO DB - export MONGO_DB=sqshq/piggymetrics-mongodb - docker build -t $MONGO_DB:$COMMIT ./mongodb - docker tag $MONGO_DB:$COMMIT $MONGO_DB:$TAG - docker push $MONGO_DB ================================================ FILE: LICENCE ================================================ The MIT License (MIT) Copyright (c) 2016 Alexander Lukyanchikov, http://sqshq.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Build Status](https://travis-ci.org/sqshq/PiggyMetrics.svg?branch=master)](https://travis-ci.org/sqshq/PiggyMetrics) [![codecov.io](https://codecov.io/github/sqshq/PiggyMetrics/coverage.svg?branch=master)](https://codecov.io/github/sqshq/PiggyMetrics?branch=master) [![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/sqshq/PiggyMetrics/blob/master/LICENCE) [![Join the chat at https://gitter.im/sqshq/PiggyMetrics](https://badges.gitter.im/sqshq/PiggyMetrics.svg)](https://gitter.im/sqshq/PiggyMetrics?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # Piggy Metrics Piggy Metrics is a simple financial advisor app built to demonstrate the [Microservice Architecture Pattern](http://martinfowler.com/microservices/) using Spring Boot, Spring Cloud and Docker. The project is intended as a tutorial, but you are welcome to fork it and turn it into something else!
![](https://cloud.githubusercontent.com/assets/6069066/13864234/442d6faa-ecb9-11e5-9929-34a9539acde0.png) ![Piggy Metrics](https://cloud.githubusercontent.com/assets/6069066/13830155/572e7552-ebe4-11e5-918f-637a49dff9a2.gif) ## Functional services Piggy Metrics is decomposed into three core microservices. All of them are independently deployable applications organized around certain business domains. Functional services #### Account service Contains general input logic and validation: incomes/expenses items, savings and account settings. Method | Path | Description | User authenticated | Available from UI ------------- | ------------------------- | ------------- |:-------------:|:----------------:| GET | /accounts/{account} | Get specified account data | | GET | /accounts/current | Get current account data | × | × GET | /accounts/demo | Get demo account data (pre-filled incomes/expenses items, etc) | | × PUT | /accounts/current | Save current account data | × | × POST | /accounts/ | Register new account | | × #### Statistics service Performs calculations on major statistics parameters and captures time series for each account. Datapoint contains values normalized to base currency and time period. This data is used to track cash flow dynamics during the account lifetime. Method | Path | Description | User authenticated | Available from UI ------------- | ------------------------- | ------------- |:-------------:|:----------------:| GET | /statistics/{account} | Get specified account statistics | | GET | /statistics/current | Get current account statistics | × | × GET | /statistics/demo | Get demo account statistics | | × PUT | /statistics/{account} | Create or update time series datapoint for specified account | | #### Notification service Stores user contact information and notification settings (reminders, backup frequency etc). Scheduled worker collects required information from other services and sends e-mail messages to subscribed customers. Method | Path | Description | User authenticated | Available from UI ------------- | ------------------------- | ------------- |:-------------:|:----------------:| GET | /notifications/settings/current | Get current account notification settings | × | × PUT | /notifications/settings/current | Save current account notification settings | × | × #### Notes - Each microservice has its own database, so there is no way to bypass API and access persistence data directly. - MongoDB is used as a primary database for each of the services. - All services are talking to each other via the Rest API ## Infrastructure [Spring cloud](https://spring.io/projects/spring-cloud) provides powerful tools for developers to quickly implement common distributed systems patterns - Infrastructure services ### Config service [Spring Cloud Config](http://cloud.spring.io/spring-cloud-config/spring-cloud-config.html) is horizontally scalable centralized configuration service for the distributed systems. It uses a pluggable repository layer that currently supports local storage, Git, and Subversion. In this project, we are going to use `native profile`, which simply loads config files from the local classpath. You can see `shared` directory in [Config service resources](https://github.com/sqshq/PiggyMetrics/tree/master/config/src/main/resources). Now, when Notification-service requests its configuration, Config service responses with `shared/notification-service.yml` and `shared/application.yml` (which is shared between all client applications). ##### Client side usage Just build Spring Boot application with `spring-cloud-starter-config` dependency, autoconfiguration will do the rest. Now you don't need any embedded properties in your application. Just provide `bootstrap.yml` with application name and Config service url: ```yml spring: application: name: notification-service cloud: config: uri: http://config:8888 fail-fast: true ``` ##### With Spring Cloud Config, you can change application config dynamically. For example, [EmailService bean](https://github.com/sqshq/PiggyMetrics/blob/master/notification-service/src/main/java/com/piggymetrics/notification/service/EmailServiceImpl.java) is annotated with `@RefreshScope`. That means you can change e-mail text and subject without rebuild and restart the Notification service. First, change required properties in Config server. Then make a refresh call to the Notification service: `curl -H "Authorization: Bearer #token#" -XPOST http://127.0.0.1:8000/notifications/refresh` You could also use Repository [webhooks to automate this process](http://cloud.spring.io/spring-cloud-config/spring-cloud-config.html#_push_notifications_and_spring_cloud_bus) ##### Notes - `@RefreshScope` doesn't work with `@Configuration` classes and doesn't ignores `@Scheduled` methods - `fail-fast` property means that Spring Boot application will fail startup immediately, if it cannot connect to the Config Service. ### Auth service Authorization responsibilities are extracted to a separate server, which grants [OAuth2 tokens](https://tools.ietf.org/html/rfc6749) for the backend resource services. Auth Server is used for user authorization as well as for secure machine-to-machine communication inside the perimeter. In this project, I use [`Password credentials`](https://tools.ietf.org/html/rfc6749#section-4.3) grant type for users authorization (since it's used only by the UI) and [`Client Credentials`](https://tools.ietf.org/html/rfc6749#section-4.4) grant for service-to-service communciation. Spring Cloud Security provides convenient annotations and autoconfiguration to make this really easy to implement on both server and client side. You can learn more about that in [documentation](http://cloud.spring.io/spring-cloud-security/spring-cloud-security.html). On the client side, everything works exactly the same as with traditional session-based authorization. You can retrieve `Principal` object from the request, check user roles using the expression-based access control and `@PreAuthorize` annotation. Each PiggyMetrics client has a scope: `server` for backend services and `ui` - for the browser. We can use `@PreAuthorize` annotation to protect controllers from an external access: ``` java @PreAuthorize("#oauth2.hasScope('server')") @RequestMapping(value = "accounts/{name}", method = RequestMethod.GET) public List getStatisticsByAccountName(@PathVariable String name) { return statisticsService.findByAccountName(name); } ``` ### API Gateway API Gateway is a single entry point into the system, used to handle requests and routing them to the appropriate backend service or by [aggregating results from a scatter-gather call](http://techblog.netflix.com/2013/01/optimizing-netflix-api.html). Also, it can be used for authentication, insights, stress and canary testing, service migration, static response handling and active traffic management. Netflix opensourced [such an edge service](http://techblog.netflix.com/2013/06/announcing-zuul-edge-service-in-cloud.html) and Spring Cloud allows to use it with a single `@EnableZuulProxy` annotation. In this project, we use Zuul to store some static content (the UI application) and to route requests to appropriate the microservices. Here's a simple prefix-based routing configuration for the Notification service: ```yml zuul: routes: notification-service: path: /notifications/** serviceId: notification-service stripPrefix: false ``` That means all requests starting with `/notifications` will be routed to the Notification service. There is no hardcoded addresses, as you can see. Zuul uses [Service discovery](https://github.com/sqshq/PiggyMetrics/blob/master/README.md#service-discovery) mechanism to locate Notification service instances and also [Circuit Breaker and Load Balancer](https://github.com/sqshq/PiggyMetrics/blob/master/README.md#http-client-load-balancer-and-circuit-breaker), described below. ### Service Discovery Service Discovery allows automatic detection of the network locations for all registered services. These locations might have dynamically assigned addresses due to auto-scaling, failures or upgrades. The key part of Service discovery is the Registry. In this project, we use Netflix Eureka. Eureka is a good example of the client-side discovery pattern, where client is responsible for looking up the locations of available service instances and load balancing between them. With Spring Boot, you can easily build Eureka Registry using the `spring-cloud-starter-eureka-server` dependency, `@EnableEurekaServer` annotation and simple configuration properties. Client support enabled with `@EnableDiscoveryClient` annotation a `bootstrap.yml` with application name: ``` yml spring: application: name: notification-service ``` This service will be registered with the Eureka Server and provided with metadata such as host, port, health indicator URL, home page etc. Eureka receives heartbeat messages from each instance belonging to the service. If the heartbeat fails over a configurable timetable, the instance will be removed from the registry. Also, Eureka provides a simple interface where you can track running services and a number of available instances: `http://localhost:8761` ### Load balancer, Circuit breaker and Http client #### Ribbon Ribbon is a client side load balancer which gives you a lot of control over the behaviour of HTTP and TCP clients. Compared to a traditional load balancer, there is no need in additional network hop - you can contact desired service directly. Out of the box, it natively integrates with Spring Cloud and Service Discovery. [Eureka Client](https://github.com/sqshq/PiggyMetrics#service-discovery) provides a dynamic list of available servers so Ribbon could balance between them. #### Hystrix Hystrix is the implementation of [Circuit Breaker Pattern](http://martinfowler.com/bliki/CircuitBreaker.html), which gives us a control over latency and network failures while communicating with other services. The main idea is to stop cascading failures in the distributed environment - that helps to fail fast and recover as soon as possible - important aspects of a fault-tolerant system that can self-heal. Moreover, Hystrix generates metrics on execution outcomes and latency for each command, that we can use to [monitor system's behavior](https://github.com/sqshq/PiggyMetrics#monitor-dashboard). #### Feign Feign is a declarative Http client which seamlessly integrates with Ribbon and Hystrix. Actually, a single `spring-cloud-starter-feign` dependency and `@EnableFeignClients` annotation gives us a full set of tools, including Load balancer, Circuit Breaker and Http client with reasonable default configuration. Here is an example from the Account Service: ``` java @FeignClient(name = "statistics-service") public interface StatisticsServiceClient { @RequestMapping(method = RequestMethod.PUT, value = "/statistics/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) void updateStatistics(@PathVariable("accountName") String accountName, Account account); } ``` - Everything you need is just an interface - You can share `@RequestMapping` part between Spring MVC controller and Feign methods - Above example specifies just a desired service id - `statistics-service`, thanks to auto-discovery through Eureka ### Monitor dashboard In this project configuration, each microservice with Hystrix on board pushes metrics to Turbine via Spring Cloud Bus (with AMQP broker). The Monitoring project is just a small Spring boot application with the [Turbine](https://github.com/Netflix/Turbine) and [Hystrix Dashboard](https://github.com/Netflix-Skunkworks/hystrix-dashboard). Let's see observe the behavior of our system under load: Statistics Service imitates a delay during the request processing. The response timeout is set to 1 second: | | | --- |--- |--- |--- | | `0 ms delay` | `500 ms delay` | `800 ms delay` | `1100 ms delay` | Well behaving system. Throughput is about 22 rps. Small number of active threads in the Statistics service. Median service time is about 50 ms. | The number of active threads is growing. We can see purple number of thread-pool rejections and therefore about 40% of errors, but the circuit is still closed. | Half-open state: the ratio of failed commands is higher than 50%, so the circuit breaker kicks in. After sleep window amount of time, the next request goes through. | 100 percent of the requests fail. The circuit is now permanently open. Retry after sleep time won't close the circuit again because a single request is too slow. ### Log analysis Centralized logging can be very useful while attempting to identify problems in a distributed environment. Elasticsearch, Logstash and Kibana stack lets you search and analyze your logs, utilization and network activity data with ease. ### Distributed tracing Analyzing problems in distributed systems can be difficult, especially trying to trace requests that propagate from one microservice to another. [Spring Cloud Sleuth](https://cloud.spring.io/spring-cloud-sleuth/) solves this problem by providing support for the distributed tracing. It adds two types of IDs to the logging: `traceId` and `spanId`. `spanId` represents a basic unit of work, for example sending an HTTP request. The traceId contains a set of spans forming a tree-like structure. For example, with a distributed big-data store, a trace might be formed by a PUT request. Using `traceId` and `spanId` for each operation we know when and where our application is as it processes a request, making reading logs much easier. The logs are as follows, notice the `[appname,traceId,spanId,exportable]` entries from the Slf4J MDC: ```text 2018-07-26 23:13:49.381 WARN [gateway,3216d0de1384bb4f,3216d0de1384bb4f,false] 2999 --- [nio-4000-exec-1] o.s.c.n.z.f.r.s.AbstractRibbonCommand : The Hystrix timeout of 20000ms for the command account-service is set lower than the combination of the Ribbon read and connect timeout, 80000ms. 2018-07-26 23:13:49.562 INFO [account-service,3216d0de1384bb4f,404ff09c5cf91d2e,false] 3079 --- [nio-6000-exec-1] c.p.account.service.AccountServiceImpl : new account has been created: test ``` - *`appname`*: The name of the application that logged the span from the property `spring.application.name` - *`traceId`*: This is an ID that is assigned to a single request, job, or action - *`spanId`*: The ID of a specific operation that took place - *`exportable`*: Whether the log should be exported to [Zipkin](https://zipkin.io/) ## Infrastructure automation Deploying microservices, with their interdependence, is much more complex process than deploying a monolithic application. It is really important to have a fully automated infrastructure. We can achieve following benefits with Continuous Delivery approach: - The ability to release software anytime - Any build could end up being a release - Build artifacts once - deploy as needed Here is a simple Continuous Delivery workflow, implemented in this project: In this [configuration](https://github.com/sqshq/PiggyMetrics/blob/master/.travis.yml), Travis CI builds tagged images for each successful git push. So, there are always the `latest` images for each microservice on [Docker Hub](https://hub.docker.com/r/sqshq/) and older images, tagged with git commit hash. It's easy to deploy any of them and quickly rollback, if needed. ## Let's try it out Note that starting 8 Spring Boot applications, 4 MongoDB instances and a RabbitMq requires at least 4Gb of RAM. #### Before you start - Install Docker and Docker Compose. - Change environment variable values in `.env` file for more security or leave it as it is. - Build the project: `mvn package [-DskipTests]` #### Production mode In this mode, all latest images will be pulled from Docker Hub. Just copy `docker-compose.yml` and hit `docker-compose up` #### Development mode If you'd like to build images yourself, you have to clone the repository and build artifacts using maven. After that, run `docker-compose -f docker-compose.yml -f docker-compose.dev.yml up` `docker-compose.dev.yml` inherits `docker-compose.yml` with additional possibility to build images locally and expose all containers ports for convenient development. If you'd like to start applications in Intellij Idea you need to either use [EnvFile plugin](https://plugins.jetbrains.com/plugin/7861-envfile) or manually export environment variables listed in `.env` file (make sure they were exported: `printenv`) #### Important endpoints - http://localhost:80 - Gateway - http://localhost:8761 - Eureka Dashboard - http://localhost:9000/hystrix - Hystrix Dashboard (Turbine stream link: `http://turbine-stream-service:8080/turbine/turbine.stream`) - http://localhost:15672 - RabbitMq management (default login/password: guest/guest) ## Contributions are welcome! PiggyMetrics is open source, and would greatly appreciate your help. Feel free to suggest and implement any improvements. ================================================ FILE: account-service/Dockerfile ================================================ FROM java:8-jre MAINTAINER Alexander Lukyanchikov ADD ./target/account-service.jar /app/ CMD ["java", "-Xmx200m", "-jar", "/app/account-service.jar"] EXPOSE 6000 ================================================ FILE: account-service/pom.xml ================================================ 4.0.0 account-service 1.0-SNAPSHOT jar account-service com.piggymetrics piggymetrics 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.boot spring-boot-starter-security org.springframework.cloud spring-cloud-starter-config org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-sleuth org.springframework.boot spring-boot-starter-data-mongodb org.springframework.boot spring-boot-starter-actuator org.springframework.cloud spring-cloud-starter-bus-amqp org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.cloud spring-cloud-netflix-hystrix-stream org.springframework.boot spring-boot-starter-test test de.flapdoodle.embed de.flapdoodle.embed.mongo 1.50.3 test com.jayway.jsonpath json-path 2.2.0 test org.springframework.boot spring-boot-maven-plugin account-service org.jacoco jacoco-maven-plugin 0.7.6.201602180812 prepare-agent report test report ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/AccountApplication.java ================================================ package com.piggymetrics.account; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; @SpringBootApplication @EnableDiscoveryClient @EnableOAuth2Client @EnableFeignClients @EnableCircuitBreaker @EnableGlobalMethodSecurity(prePostEnabled = true) public class AccountApplication { public static void main(String[] args) { SpringApplication.run(AccountApplication.class, args); } } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/client/AuthServiceClient.java ================================================ package com.piggymetrics.account.client; import com.piggymetrics.account.domain.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @FeignClient(name = "auth-service") public interface AuthServiceClient { @RequestMapping(method = RequestMethod.POST, value = "/uaa/users", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) void createUser(User user); } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/client/StatisticsServiceClient.java ================================================ package com.piggymetrics.account.client; import com.piggymetrics.account.domain.Account; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @FeignClient(name = "statistics-service", fallback = StatisticsServiceClientFallback.class) public interface StatisticsServiceClient { @RequestMapping(method = RequestMethod.PUT, value = "/statistics/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) void updateStatistics(@PathVariable("accountName") String accountName, Account account); } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/client/StatisticsServiceClientFallback.java ================================================ package com.piggymetrics.account.client; import com.piggymetrics.account.domain.Account; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; /** * @author cdov */ @Component public class StatisticsServiceClientFallback implements StatisticsServiceClient { private static final Logger LOGGER = LoggerFactory.getLogger(StatisticsServiceClientFallback.class); @Override public void updateStatistics(String accountName, Account account) { LOGGER.error("Error during update statistics for account: {}", accountName); } } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/config/ResourceServerConfig.java ================================================ package com.piggymetrics.account.config; import com.piggymetrics.account.service.security.CustomUserInfoTokenServices; import feign.RequestInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; /** * @author cdov */ @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private final ResourceServerProperties sso; @Autowired public ResourceServerConfig(ResourceServerProperties sso) { this.sso = sso; } @Bean @ConfigurationProperties(prefix = "security.oauth2.client") public ClientCredentialsResourceDetails clientCredentialsResourceDetails() { return new ClientCredentialsResourceDetails(); } @Bean public RequestInterceptor oauth2FeignRequestInterceptor(){ return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails()); } @Bean public OAuth2RestTemplate clientCredentialsRestTemplate() { return new OAuth2RestTemplate(clientCredentialsResourceDetails()); } @Bean public ResourceServerTokenServices tokenServices() { return new CustomUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId()); } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/" , "/demo").permitAll() .anyRequest().authenticated(); } } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/controller/AccountController.java ================================================ package com.piggymetrics.account.controller; import com.piggymetrics.account.domain.Account; import com.piggymetrics.account.domain.User; import com.piggymetrics.account.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.security.Principal; @RestController public class AccountController { @Autowired private AccountService accountService; @PreAuthorize("#oauth2.hasScope('server') or #name.equals('demo')") @RequestMapping(path = "/{name}", method = RequestMethod.GET) public Account getAccountByName(@PathVariable String name) { return accountService.findByName(name); } @RequestMapping(path = "/current", method = RequestMethod.GET) public Account getCurrentAccount(Principal principal) { return accountService.findByName(principal.getName()); } @RequestMapping(path = "/current", method = RequestMethod.PUT) public void saveCurrentAccount(Principal principal, @Valid @RequestBody Account account) { accountService.saveChanges(principal.getName(), account); } @RequestMapping(path = "/", method = RequestMethod.POST) public Account createNewAccount(@Valid @RequestBody User user) { return accountService.create(user); } } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/controller/ErrorHandler.java ================================================ package com.piggymetrics.account.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @ControllerAdvice public class ErrorHandler { private final Logger log = LoggerFactory.getLogger(getClass()); // TODO add MethodArgumentNotValidException handler // TODO remove such general handler @ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public void processValidationError(IllegalArgumentException e) { log.info("Returning HTTP 400 Bad Request", e); } } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/domain/Account.java ================================================ package com.piggymetrics.account.domain; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.hibernate.validator.constraints.Length; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.util.Date; import java.util.List; @Document(collection = "accounts") @JsonIgnoreProperties(ignoreUnknown = true) public class Account { @Id private String name; private Date lastSeen; @Valid private List incomes; @Valid private List expenses; @Valid @NotNull private Saving saving; @Length(min = 0, max = 20_000) private String note; public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getLastSeen() { return lastSeen; } public void setLastSeen(Date lastSeen) { this.lastSeen = lastSeen; } public List getIncomes() { return incomes; } public void setIncomes(List incomes) { this.incomes = incomes; } public List getExpenses() { return expenses; } public void setExpenses(List expenses) { this.expenses = expenses; } public Saving getSaving() { return saving; } public void setSaving(Saving saving) { this.saving = saving; } public String getNote() { return note; } public void setNote(String note) { this.note = note; } } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/domain/Currency.java ================================================ package com.piggymetrics.account.domain; public enum Currency { USD, EUR, RUB; public static Currency getDefault() { return USD; } } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/domain/Item.java ================================================ package com.piggymetrics.account.domain; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; import java.math.BigDecimal; public class Item { @NotNull @Length(min = 1, max = 20) private String title; @NotNull private BigDecimal amount; @NotNull private Currency currency; @NotNull private TimePeriod period; @NotNull private String icon; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public BigDecimal getAmount() { return amount; } public void setAmount(BigDecimal amount) { this.amount = amount; } public Currency getCurrency() { return currency; } public void setCurrency(Currency currency) { this.currency = currency; } public TimePeriod getPeriod() { return period; } public void setPeriod(TimePeriod period) { this.period = period; } public String getIcon() { return icon; } public void setIcon(String icon) { this.icon = icon; } } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/domain/Saving.java ================================================ package com.piggymetrics.account.domain; import javax.validation.constraints.NotNull; import java.math.BigDecimal; public class Saving { @NotNull private BigDecimal amount; @NotNull private Currency currency; @NotNull private BigDecimal interest; @NotNull private Boolean deposit; @NotNull private Boolean capitalization; public BigDecimal getAmount() { return amount; } public void setAmount(BigDecimal amount) { this.amount = amount; } public Currency getCurrency() { return currency; } public void setCurrency(Currency currency) { this.currency = currency; } public BigDecimal getInterest() { return interest; } public void setInterest(BigDecimal interest) { this.interest = interest; } public Boolean getDeposit() { return deposit; } public void setDeposit(Boolean deposit) { this.deposit = deposit; } public Boolean getCapitalization() { return capitalization; } public void setCapitalization(Boolean capitalization) { this.capitalization = capitalization; } } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/domain/TimePeriod.java ================================================ package com.piggymetrics.account.domain; public enum TimePeriod { YEAR, QUARTER, MONTH, DAY, HOUR } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/domain/User.java ================================================ package com.piggymetrics.account.domain; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; public class User { @NotNull @Length(min = 3, max = 20) private String username; @NotNull @Length(min = 6, max = 40) private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/repository/AccountRepository.java ================================================ package com.piggymetrics.account.repository; import com.piggymetrics.account.domain.Account; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository public interface AccountRepository extends CrudRepository { Account findByName(String name); } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/service/AccountService.java ================================================ package com.piggymetrics.account.service; import com.piggymetrics.account.domain.Account; import com.piggymetrics.account.domain.User; public interface AccountService { /** * Finds account by given name * * @param accountName * @return found account */ Account findByName(String accountName); /** * Checks if account with the same name already exists * Invokes Auth Service user creation * Creates new account with default parameters * * @param user * @return created account */ Account create(User user); /** * Validates and applies incoming account updates * Invokes Statistics Service update * * @param name * @param update */ void saveChanges(String name, Account update); } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/service/AccountServiceImpl.java ================================================ package com.piggymetrics.account.service; import com.piggymetrics.account.client.AuthServiceClient; import com.piggymetrics.account.client.StatisticsServiceClient; import com.piggymetrics.account.domain.Account; import com.piggymetrics.account.domain.Currency; import com.piggymetrics.account.domain.Saving; import com.piggymetrics.account.domain.User; import com.piggymetrics.account.repository.AccountRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import java.math.BigDecimal; import java.util.Date; @Service public class AccountServiceImpl implements AccountService { private final Logger log = LoggerFactory.getLogger(getClass()); @Autowired private StatisticsServiceClient statisticsClient; @Autowired private AuthServiceClient authClient; @Autowired private AccountRepository repository; /** * {@inheritDoc} */ @Override public Account findByName(String accountName) { Assert.hasLength(accountName); return repository.findByName(accountName); } /** * {@inheritDoc} */ @Override public Account create(User user) { Account existing = repository.findByName(user.getUsername()); Assert.isNull(existing, "account already exists: " + user.getUsername()); authClient.createUser(user); Saving saving = new Saving(); saving.setAmount(new BigDecimal(0)); saving.setCurrency(Currency.getDefault()); saving.setInterest(new BigDecimal(0)); saving.setDeposit(false); saving.setCapitalization(false); Account account = new Account(); account.setName(user.getUsername()); account.setLastSeen(new Date()); account.setSaving(saving); repository.save(account); log.info("new account has been created: " + account.getName()); return account; } /** * {@inheritDoc} */ @Override public void saveChanges(String name, Account update) { Account account = repository.findByName(name); Assert.notNull(account, "can't find account with name " + name); account.setIncomes(update.getIncomes()); account.setExpenses(update.getExpenses()); account.setSaving(update.getSaving()); account.setNote(update.getNote()); account.setLastSeen(new Date()); repository.save(account); log.debug("account {} changes has been saved", name); statisticsClient.updateStatistics(name, account); } } ================================================ FILE: account-service/src/main/java/com/piggymetrics/account/service/security/CustomUserInfoTokenServices.java ================================================ package com.piggymetrics.account.service.security; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor; import org.springframework.boot.autoconfigure.security.oauth2.resource.FixedAuthoritiesExtractor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.client.OAuth2RestOperations; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.OAuth2Request; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; import java.util.*; /** * Extended implementation of {@link org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices} * * By default, it designed to return only user details. This class provides {@link #getRequest(Map)} method, which * returns clientId and scope of calling service. This information used in controller's security checks. */ public class CustomUserInfoTokenServices implements ResourceServerTokenServices { protected final Log logger = LogFactory.getLog(getClass()); private static final String[] PRINCIPAL_KEYS = new String[] { "user", "username", "userid", "user_id", "login", "id", "name" }; private final String userInfoEndpointUrl; private final String clientId; private OAuth2RestOperations restTemplate; private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE; private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor(); public CustomUserInfoTokenServices(String userInfoEndpointUrl, String clientId) { this.userInfoEndpointUrl = userInfoEndpointUrl; this.clientId = clientId; } public void setTokenType(String tokenType) { this.tokenType = tokenType; } public void setRestTemplate(OAuth2RestOperations restTemplate) { this.restTemplate = restTemplate; } public void setAuthoritiesExtractor(AuthoritiesExtractor authoritiesExtractor) { this.authoritiesExtractor = authoritiesExtractor; } @Override public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { Map map = getMap(this.userInfoEndpointUrl, accessToken); if (map.containsKey("error")) { this.logger.debug("userinfo returned error: " + map.get("error")); throw new InvalidTokenException(accessToken); } return extractAuthentication(map); } private OAuth2Authentication extractAuthentication(Map map) { Object principal = getPrincipal(map); OAuth2Request request = getRequest(map); List authorities = this.authoritiesExtractor .extractAuthorities(map); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( principal, "N/A", authorities); token.setDetails(map); return new OAuth2Authentication(request, token); } private Object getPrincipal(Map map) { for (String key : PRINCIPAL_KEYS) { if (map.containsKey(key)) { return map.get(key); } } return "unknown"; } @SuppressWarnings({ "unchecked" }) private OAuth2Request getRequest(Map map) { Map request = (Map) map.get("oauth2Request"); String clientId = (String) request.get("clientId"); Set scope = new LinkedHashSet<>(request.containsKey("scope") ? (Collection) request.get("scope") : Collections.emptySet()); return new OAuth2Request(null, clientId, null, true, new HashSet<>(scope), null, null, null, null); } @Override public OAuth2AccessToken readAccessToken(String accessToken) { throw new UnsupportedOperationException("Not supported: read access token"); } @SuppressWarnings({ "unchecked" }) private Map getMap(String path, String accessToken) { this.logger.debug("Getting user info from: " + path); try { OAuth2RestOperations restTemplate = this.restTemplate; if (restTemplate == null) { BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails(); resource.setClientId(this.clientId); restTemplate = new OAuth2RestTemplate(resource); } OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext() .getAccessToken(); if (existingToken == null || !accessToken.equals(existingToken.getValue())) { DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken( accessToken); token.setTokenType(this.tokenType); restTemplate.getOAuth2ClientContext().setAccessToken(token); } return restTemplate.getForEntity(path, Map.class).getBody(); } catch (Exception ex) { this.logger.info("Could not fetch user details: " + ex.getClass() + ", " + ex.getMessage()); return Collections.singletonMap("error", "Could not fetch user details"); } } } ================================================ FILE: account-service/src/main/resources/bootstrap.yml ================================================ spring: application: name: account-service cloud: config: uri: http://config:8888 fail-fast: true password: ${CONFIG_SERVICE_PASSWORD} username: user ================================================ FILE: account-service/src/test/java/com/piggymetrics/account/AccountServiceApplicationTests.java ================================================ package com.piggymetrics.account; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class AccountServiceApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: account-service/src/test/java/com/piggymetrics/account/client/StatisticsServiceClientFallbackTest.java ================================================ package com.piggymetrics.account.client; import com.piggymetrics.account.domain.Account; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.rule.OutputCapture; import org.springframework.test.context.junit4.SpringRunner; import static org.hamcrest.Matchers.containsString; /** * @author cdov */ @RunWith(SpringRunner.class) @SpringBootTest(properties = { "feign.hystrix.enabled=true" }) public class StatisticsServiceClientFallbackTest { @Autowired private StatisticsServiceClient statisticsServiceClient; @Rule public final OutputCapture outputCapture = new OutputCapture(); @Before public void setup() { outputCapture.reset(); } @Test public void testUpdateStatisticsWithFailFallback(){ statisticsServiceClient.updateStatistics("test", new Account()); outputCapture.expect(containsString("Error during update statistics for account: test")); } } ================================================ FILE: account-service/src/test/java/com/piggymetrics/account/controller/AccountControllerTest.java ================================================ package com.piggymetrics.account.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.piggymetrics.account.domain.*; import com.piggymetrics.account.service.AccountService; import com.sun.security.auth.UserPrincipal; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.math.BigDecimal; import java.util.Date; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @SpringBootTest public class AccountControllerTest { private static final ObjectMapper mapper = new ObjectMapper(); @InjectMocks private AccountController accountController; @Mock private AccountService accountService; private MockMvc mockMvc; @Before public void setup() { initMocks(this); this.mockMvc = MockMvcBuilders.standaloneSetup(accountController).build(); } @Test public void shouldGetAccountByName() throws Exception { final Account account = new Account(); account.setName("test"); when(accountService.findByName(account.getName())).thenReturn(account); mockMvc.perform(get("/" + account.getName())) .andExpect(jsonPath("$.name").value(account.getName())) .andExpect(status().isOk()); } @Test public void shouldGetCurrentAccount() throws Exception { final Account account = new Account(); account.setName("test"); when(accountService.findByName(account.getName())).thenReturn(account); mockMvc.perform(get("/current").principal(new UserPrincipal(account.getName()))) .andExpect(jsonPath("$.name").value(account.getName())) .andExpect(status().isOk()); } @Test public void shouldSaveCurrentAccount() throws Exception { Saving saving = new Saving(); saving.setAmount(new BigDecimal(1500)); saving.setCurrency(Currency.USD); saving.setInterest(new BigDecimal("3.32")); saving.setDeposit(true); saving.setCapitalization(false); Item grocery = new Item(); grocery.setTitle("Grocery"); grocery.setAmount(new BigDecimal(10)); grocery.setCurrency(Currency.USD); grocery.setPeriod(TimePeriod.DAY); grocery.setIcon("meal"); Item salary = new Item(); salary.setTitle("Salary"); salary.setAmount(new BigDecimal(9100)); salary.setCurrency(Currency.USD); salary.setPeriod(TimePeriod.MONTH); salary.setIcon("wallet"); final Account account = new Account(); account.setName("test"); account.setNote("test note"); account.setLastSeen(new Date()); account.setSaving(saving); account.setExpenses(ImmutableList.of(grocery)); account.setIncomes(ImmutableList.of(salary)); String json = mapper.writeValueAsString(account); mockMvc.perform(put("/current").principal(new UserPrincipal(account.getName())).contentType(MediaType.APPLICATION_JSON).content(json)) .andExpect(status().isOk()); } @Test public void shouldFailOnValidationTryingToSaveCurrentAccount() throws Exception { final Account account = new Account(); account.setName("test"); String json = mapper.writeValueAsString(account); mockMvc.perform(put("/current").principal(new UserPrincipal(account.getName())).contentType(MediaType.APPLICATION_JSON).content(json)) .andExpect(status().isBadRequest()); } @Test public void shouldRegisterNewAccount() throws Exception { final User user = new User(); user.setUsername("test"); user.setPassword("password"); String json = mapper.writeValueAsString(user); System.out.println(json); mockMvc.perform(post("/").principal(new UserPrincipal("test")).contentType(MediaType.APPLICATION_JSON).content(json)) .andExpect(status().isOk()); } @Test public void shouldFailOnValidationTryingToRegisterNewAccount() throws Exception { final User user = new User(); user.setUsername("t"); String json = mapper.writeValueAsString(user); mockMvc.perform(post("/").principal(new UserPrincipal("test")).contentType(MediaType.APPLICATION_JSON).content(json)) .andExpect(status().isBadRequest()); } } ================================================ FILE: account-service/src/test/java/com/piggymetrics/account/repository/AccountRepositoryTest.java ================================================ package com.piggymetrics.account.repository; import com.piggymetrics.account.domain.Account; import com.piggymetrics.account.domain.Currency; import com.piggymetrics.account.domain.Item; import com.piggymetrics.account.domain.Saving; import com.piggymetrics.account.domain.TimePeriod; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; import org.springframework.test.context.junit4.SpringRunner; import java.math.BigDecimal; import java.util.Arrays; import java.util.Date; import static org.junit.Assert.assertEquals; @RunWith(SpringRunner.class) @DataMongoTest public class AccountRepositoryTest { @Autowired private AccountRepository repository; @Test public void shouldFindAccountByName() { Account stub = getStubAccount(); repository.save(stub); Account found = repository.findByName(stub.getName()); assertEquals(stub.getLastSeen(), found.getLastSeen()); assertEquals(stub.getNote(), found.getNote()); assertEquals(stub.getIncomes().size(), found.getIncomes().size()); assertEquals(stub.getExpenses().size(), found.getExpenses().size()); } private Account getStubAccount() { Saving saving = new Saving(); saving.setAmount(new BigDecimal(1500)); saving.setCurrency(Currency.USD); saving.setInterest(new BigDecimal("3.32")); saving.setDeposit(true); saving.setCapitalization(false); Item vacation = new Item(); vacation.setTitle("Vacation"); vacation.setAmount(new BigDecimal(3400)); vacation.setCurrency(Currency.EUR); vacation.setPeriod(TimePeriod.YEAR); vacation.setIcon("tourism"); Item grocery = new Item(); grocery.setTitle("Grocery"); grocery.setAmount(new BigDecimal(10)); grocery.setCurrency(Currency.USD); grocery.setPeriod(TimePeriod.DAY); grocery.setIcon("meal"); Item salary = new Item(); salary.setTitle("Salary"); salary.setAmount(new BigDecimal(9100)); salary.setCurrency(Currency.USD); salary.setPeriod(TimePeriod.MONTH); salary.setIcon("wallet"); Account account = new Account(); account.setName("test"); account.setNote("test note"); account.setLastSeen(new Date()); account.setSaving(saving); account.setExpenses(Arrays.asList(grocery, vacation)); account.setIncomes(Arrays.asList(salary)); return account; } } ================================================ FILE: account-service/src/test/java/com/piggymetrics/account/service/AccountServiceTest.java ================================================ package com.piggymetrics.account.service; import com.piggymetrics.account.client.AuthServiceClient; import com.piggymetrics.account.client.StatisticsServiceClient; import com.piggymetrics.account.domain.*; import com.piggymetrics.account.repository.AccountRepository; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.math.BigDecimal; import java.util.Arrays; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; public class AccountServiceTest { @InjectMocks private AccountServiceImpl accountService; @Mock private StatisticsServiceClient statisticsClient; @Mock private AuthServiceClient authClient; @Mock private AccountRepository repository; @Before public void setup() { initMocks(this); } @Test public void shouldFindByName() { final Account account = new Account(); account.setName("test"); when(accountService.findByName(account.getName())).thenReturn(account); Account found = accountService.findByName(account.getName()); assertEquals(account, found); } @Test(expected = IllegalArgumentException.class) public void shouldFailWhenNameIsEmpty() { accountService.findByName(""); } @Test public void shouldCreateAccountWithGivenUser() { User user = new User(); user.setUsername("test"); Account account = accountService.create(user); assertEquals(user.getUsername(), account.getName()); assertEquals(0, account.getSaving().getAmount().intValue()); assertEquals(Currency.getDefault(), account.getSaving().getCurrency()); assertEquals(0, account.getSaving().getInterest().intValue()); assertEquals(false, account.getSaving().getDeposit()); assertEquals(false, account.getSaving().getCapitalization()); assertNotNull(account.getLastSeen()); verify(authClient, times(1)).createUser(user); verify(repository, times(1)).save(account); } @Test public void shouldSaveChangesWhenUpdatedAccountGiven() { Item grocery = new Item(); grocery.setTitle("Grocery"); grocery.setAmount(new BigDecimal(10)); grocery.setCurrency(Currency.USD); grocery.setPeriod(TimePeriod.DAY); grocery.setIcon("meal"); Item salary = new Item(); salary.setTitle("Salary"); salary.setAmount(new BigDecimal(9100)); salary.setCurrency(Currency.USD); salary.setPeriod(TimePeriod.MONTH); salary.setIcon("wallet"); Saving saving = new Saving(); saving.setAmount(new BigDecimal(1500)); saving.setCurrency(Currency.USD); saving.setInterest(new BigDecimal("3.32")); saving.setDeposit(true); saving.setCapitalization(false); final Account update = new Account(); update.setName("test"); update.setNote("test note"); update.setIncomes(Arrays.asList(salary)); update.setExpenses(Arrays.asList(grocery)); update.setSaving(saving); final Account account = new Account(); when(accountService.findByName("test")).thenReturn(account); accountService.saveChanges("test", update); assertEquals(update.getNote(), account.getNote()); assertNotNull(account.getLastSeen()); assertEquals(update.getSaving().getAmount(), account.getSaving().getAmount()); assertEquals(update.getSaving().getCurrency(), account.getSaving().getCurrency()); assertEquals(update.getSaving().getInterest(), account.getSaving().getInterest()); assertEquals(update.getSaving().getDeposit(), account.getSaving().getDeposit()); assertEquals(update.getSaving().getCapitalization(), account.getSaving().getCapitalization()); assertEquals(update.getExpenses().size(), account.getExpenses().size()); assertEquals(update.getIncomes().size(), account.getIncomes().size()); assertEquals(update.getExpenses().get(0).getTitle(), account.getExpenses().get(0).getTitle()); assertEquals(0, update.getExpenses().get(0).getAmount().compareTo(account.getExpenses().get(0).getAmount())); assertEquals(update.getExpenses().get(0).getCurrency(), account.getExpenses().get(0).getCurrency()); assertEquals(update.getExpenses().get(0).getPeriod(), account.getExpenses().get(0).getPeriod()); assertEquals(update.getExpenses().get(0).getIcon(), account.getExpenses().get(0).getIcon()); assertEquals(update.getIncomes().get(0).getTitle(), account.getIncomes().get(0).getTitle()); assertEquals(0, update.getIncomes().get(0).getAmount().compareTo(account.getIncomes().get(0).getAmount())); assertEquals(update.getIncomes().get(0).getCurrency(), account.getIncomes().get(0).getCurrency()); assertEquals(update.getIncomes().get(0).getPeriod(), account.getIncomes().get(0).getPeriod()); assertEquals(update.getIncomes().get(0).getIcon(), account.getIncomes().get(0).getIcon()); verify(repository, times(1)).save(account); verify(statisticsClient, times(1)).updateStatistics("test", account); } @Test(expected = IllegalArgumentException.class) public void shouldFailWhenNoAccountsExistedWithGivenName() { final Account update = new Account(); update.setIncomes(Arrays.asList(new Item())); update.setExpenses(Arrays.asList(new Item())); when(accountService.findByName("test")).thenReturn(null); accountService.saveChanges("test", update); } } ================================================ FILE: account-service/src/test/resources/application.yml ================================================ spring: data: mongodb: database: piggymetrics port: 0 ================================================ FILE: account-service/src/test/resources/bootstrap.yml ================================================ eureka: client: enabled: false ================================================ FILE: auth-service/Dockerfile ================================================ FROM java:8-jre MAINTAINER Alexander Lukyanchikov ADD ./target/auth-service.jar /app/ CMD ["java", "-Xmx200m", "-jar", "/app/auth-service.jar"] EXPOSE 5000 ================================================ FILE: auth-service/pom.xml ================================================ 4.0.0 auth-service 1.0-SNAPSHOT jar auth-service com.piggymetrics piggymetrics 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-data-mongodb org.springframework.cloud spring-cloud-starter-config org.springframework.boot spring-boot-starter-security org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-sleuth org.springframework.boot spring-boot-starter-test test de.flapdoodle.embed de.flapdoodle.embed.mongo 1.50.3 test com.jayway.jsonpath json-path 2.2.0 test org.springframework.boot spring-boot-maven-plugin auth-service org.jacoco jacoco-maven-plugin 0.7.6.201602180812 prepare-agent report test report ================================================ FILE: auth-service/src/main/java/com/piggymetrics/auth/AuthApplication.java ================================================ package com.piggymetrics.auth; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; @SpringBootApplication @EnableResourceServer @EnableDiscoveryClient @EnableGlobalMethodSecurity(prePostEnabled = true) public class AuthApplication { public static void main(String[] args) { SpringApplication.run(AuthApplication.class, args); } } ================================================ FILE: auth-service/src/main/java/com/piggymetrics/auth/config/OAuth2AuthorizationConfig.java ================================================ package com.piggymetrics.auth.config; import com.piggymetrics.auth.service.security.MongoUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; /** * @author cdov */ @Configuration @EnableAuthorizationServer public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter { private TokenStore tokenStore = new InMemoryTokenStore(); private final String NOOP_PASSWORD_ENCODE = "{noop}"; @Autowired @Qualifier("authenticationManagerBean") private AuthenticationManager authenticationManager; @Autowired private MongoUserDetailsService userDetailsService; @Autowired private Environment env; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // TODO persist clients details // @formatter:off clients.inMemory() .withClient("browser") .authorizedGrantTypes("refresh_token", "password") .scopes("ui") .and() .withClient("account-service") .secret(env.getProperty("ACCOUNT_SERVICE_PASSWORD")) .authorizedGrantTypes("client_credentials", "refresh_token") .scopes("server") .and() .withClient("statistics-service") .secret(env.getProperty("STATISTICS_SERVICE_PASSWORD")) .authorizedGrantTypes("client_credentials", "refresh_token") .scopes("server") .and() .withClient("notification-service") .secret(env.getProperty("NOTIFICATION_SERVICE_PASSWORD")) .authorizedGrantTypes("client_credentials", "refresh_token") .scopes("server"); // @formatter:on } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(tokenStore) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); } @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()") .passwordEncoder(NoOpPasswordEncoder.getInstance()); } } ================================================ FILE: auth-service/src/main/java/com/piggymetrics/auth/config/WebSecurityConfig.java ================================================ package com.piggymetrics.auth.config; import com.piggymetrics.auth.service.security.MongoUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** * @author cdov */ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MongoUserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http .authorizeRequests().anyRequest().authenticated() .and() .csrf().disable(); // @formatter:on } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(new BCryptPasswordEncoder()); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } } ================================================ FILE: auth-service/src/main/java/com/piggymetrics/auth/controller/UserController.java ================================================ package com.piggymetrics.auth.controller; import com.piggymetrics.auth.domain.User; import com.piggymetrics.auth.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; import java.security.Principal; @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @RequestMapping(value = "/current", method = RequestMethod.GET) public Principal getUser(Principal principal) { return principal; } @PreAuthorize("#oauth2.hasScope('server')") @RequestMapping(method = RequestMethod.POST) public void createUser(@Valid @RequestBody User user) { userService.create(user); } } ================================================ FILE: auth-service/src/main/java/com/piggymetrics/auth/domain/User.java ================================================ package com.piggymetrics.auth.domain; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.List; @Document(collection = "users") public class User implements UserDetails { @Id private String username; private String password; @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public List getAuthorities() { return null; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } } ================================================ FILE: auth-service/src/main/java/com/piggymetrics/auth/repository/UserRepository.java ================================================ package com.piggymetrics.auth.repository; import com.piggymetrics.auth.domain.User; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository public interface UserRepository extends CrudRepository { } ================================================ FILE: auth-service/src/main/java/com/piggymetrics/auth/service/UserService.java ================================================ package com.piggymetrics.auth.service; import com.piggymetrics.auth.domain.User; public interface UserService { void create(User user); } ================================================ FILE: auth-service/src/main/java/com/piggymetrics/auth/service/UserServiceImpl.java ================================================ package com.piggymetrics.auth.service; import com.piggymetrics.auth.domain.User; import com.piggymetrics.auth.repository.UserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import java.util.Optional; @Service public class UserServiceImpl implements UserService { private final Logger log = LoggerFactory.getLogger(getClass()); private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); @Autowired private UserRepository repository; @Override public void create(User user) { Optional existing = repository.findById(user.getUsername()); existing.ifPresent(it-> {throw new IllegalArgumentException("user already exists: " + it.getUsername());}); String hash = encoder.encode(user.getPassword()); user.setPassword(hash); repository.save(user); log.info("new user has been created: {}", user.getUsername()); } } ================================================ FILE: auth-service/src/main/java/com/piggymetrics/auth/service/security/MongoUserDetailsService.java ================================================ package com.piggymetrics.auth.service.security; import com.piggymetrics.auth.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service public class MongoUserDetailsService implements UserDetailsService { @Autowired private UserRepository repository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return repository.findById(username).orElseThrow(()->new UsernameNotFoundException(username)); } } ================================================ FILE: auth-service/src/main/resources/bootstrap.yml ================================================ spring: application: name: auth-service cloud: config: uri: http://config:8888 fail-fast: true password: ${CONFIG_SERVICE_PASSWORD} username: user ================================================ FILE: auth-service/src/test/java/com/piggymetrics/auth/AuthServiceApplicationTests.java ================================================ package com.piggymetrics.auth; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class AuthServiceApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: auth-service/src/test/java/com/piggymetrics/auth/controller/UserControllerTest.java ================================================ package com.piggymetrics.auth.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.piggymetrics.auth.domain.User; import com.piggymetrics.auth.service.UserService; import com.sun.security.auth.UserPrincipal; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.mockito.MockitoAnnotations.initMocks; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @SpringBootTest public class UserControllerTest { private static final ObjectMapper mapper = new ObjectMapper(); @InjectMocks private UserController accountController; @Mock private UserService userService; private MockMvc mockMvc; @Before public void setup() { initMocks(this); this.mockMvc = MockMvcBuilders.standaloneSetup(accountController).build(); } @Test public void shouldCreateNewUser() throws Exception { final User user = new User(); user.setUsername("test"); user.setPassword("password"); String json = mapper.writeValueAsString(user); mockMvc.perform(post("/users").contentType(MediaType.APPLICATION_JSON).content(json)) .andExpect(status().isOk()); } @Test public void shouldFailWhenUserIsNotValid() throws Exception { final User user = new User(); user.setUsername("t"); user.setPassword("p"); mockMvc.perform(post("/users")) .andExpect(status().isBadRequest()); } @Test public void shouldReturnCurrentUser() throws Exception { mockMvc.perform(get("/users/current").principal(new UserPrincipal("test"))) .andExpect(jsonPath("$.name").value("test")) .andExpect(status().isOk()); } } ================================================ FILE: auth-service/src/test/java/com/piggymetrics/auth/repository/UserRepositoryTest.java ================================================ package com.piggymetrics.auth.repository; import com.piggymetrics.auth.domain.User; import com.piggymetrics.auth.service.security.MongoUserDetailsService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration; import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit4.SpringRunner; import java.util.Optional; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @RunWith(SpringRunner.class) @DataMongoTest public class UserRepositoryTest { @Autowired private UserRepository repository; @Test public void shouldSaveAndFindUserByName() { User user = new User(); user.setUsername("name"); user.setPassword("password"); repository.save(user); Optional found = repository.findById(user.getUsername()); assertTrue(found.isPresent()); assertEquals(user.getUsername(), found.get().getUsername()); assertEquals(user.getPassword(), found.get().getPassword()); } } ================================================ FILE: auth-service/src/test/java/com/piggymetrics/auth/service/UserServiceTest.java ================================================ package com.piggymetrics.auth.service; import com.piggymetrics.auth.domain.User; import com.piggymetrics.auth.repository.UserRepository; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.util.Optional; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; public class UserServiceTest { @InjectMocks private UserServiceImpl userService; @Mock private UserRepository repository; @Before public void setup() { initMocks(this); } @Test public void shouldCreateUser() { User user = new User(); user.setUsername("name"); user.setPassword("password"); userService.create(user); verify(repository, times(1)).save(user); } @Test(expected = IllegalArgumentException.class) public void shouldFailWhenUserAlreadyExists() { User user = new User(); user.setUsername("name"); user.setPassword("password"); when(repository.findById(user.getUsername())).thenReturn(Optional.of(new User())); userService.create(user); } } ================================================ FILE: auth-service/src/test/java/com/piggymetrics/auth/service/security/MongoUserDetailsServiceTest.java ================================================ package com.piggymetrics.auth.service.security; import com.piggymetrics.auth.domain.User; import com.piggymetrics.auth.repository.UserRepository; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import java.util.Optional; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; public class MongoUserDetailsServiceTest { @InjectMocks private MongoUserDetailsService service; @Mock private UserRepository repository; @Before public void setup() { initMocks(this); } @Test public void shouldLoadByUsernameWhenUserExists() { final User user = new User(); when(repository.findById(any())).thenReturn(Optional.of(user)); UserDetails loaded = service.loadUserByUsername("name"); assertEquals(user, loaded); } @Test(expected = UsernameNotFoundException.class) public void shouldFailToLoadByUsernameWhenUserNotExists() { service.loadUserByUsername("name"); } } ================================================ FILE: auth-service/src/test/resources/application.yml ================================================ spring: data: mongodb: database: piggymetrics port: 0 ================================================ FILE: auth-service/src/test/resources/bootstrap.yml ================================================ eureka: client: enabled: false ================================================ FILE: config/Dockerfile ================================================ FROM java:8-jre MAINTAINER Alexander Lukyanchikov ADD ./target/config.jar /app/ CMD ["java", "-Xmx200m", "-jar", "/app/config.jar"] HEALTHCHECK --interval=30s --timeout=30s CMD curl -f http://localhost:8888/actuator/health || exit 1 EXPOSE 8888 ================================================ FILE: config/pom.xml ================================================ 4.0.0 config 1.0.0-SNAPSHOT jar config Configuration Server com.piggymetrics piggymetrics 1.0-SNAPSHOT org.springframework.cloud spring-cloud-config-server org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-maven-plugin config ================================================ FILE: config/src/main/java/com/piggymetrics/config/ConfigApplication.java ================================================ package com.piggymetrics.config; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.config.server.EnableConfigServer; @SpringBootApplication @EnableConfigServer public class ConfigApplication { public static void main(String[] args) { SpringApplication.run(ConfigApplication.class, args); } } ================================================ FILE: config/src/main/java/com/piggymetrics/config/SecurityConfig.java ================================================ package com.piggymetrics.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * @author cdov */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http .authorizeRequests() .antMatchers("/actuator/**").permitAll() .anyRequest().authenticated() .and() .httpBasic() ; } } ================================================ FILE: config/src/main/resources/application.yml ================================================ spring: cloud: config: server: native: search-locations: classpath:/shared profiles: active: native security: user: password: ${CONFIG_SERVICE_PASSWORD} server: port: 8888 ================================================ FILE: config/src/main/resources/shared/account-service.yml ================================================ security: oauth2: client: clientId: account-service clientSecret: ${ACCOUNT_SERVICE_PASSWORD} accessTokenUri: http://auth-service:5000/uaa/oauth/token grant-type: client_credentials scope: server spring: data: mongodb: host: account-mongodb username: user password: ${MONGODB_PASSWORD} database: piggymetrics port: 27017 server: servlet: context-path: /accounts port: 6000 feign: hystrix: enabled: true ================================================ FILE: config/src/main/resources/shared/application.yml ================================================ logging: level: org.springframework.security: INFO hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 10000 eureka: instance: prefer-ip-address: true client: serviceUrl: defaultZone: http://registry:8761/eureka/ security: oauth2: resource: user-info-uri: http://auth-service:5000/uaa/users/current spring: rabbitmq: host: rabbitmq ================================================ FILE: config/src/main/resources/shared/auth-service.yml ================================================ spring: data: mongodb: host: auth-mongodb username: user password: ${MONGODB_PASSWORD} database: piggymetrics port: 27017 server: servlet: context-path: /uaa port: 5000 ================================================ FILE: config/src/main/resources/shared/gateway.yml ================================================ hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 20000 ribbon: ReadTimeout: 20000 ConnectTimeout: 20000 zuul: ignoredServices: '*' host: connect-timeout-millis: 20000 socket-timeout-millis: 20000 routes: auth-service: path: /uaa/** url: http://auth-service:5000 stripPrefix: false sensitiveHeaders: account-service: path: /accounts/** serviceId: account-service stripPrefix: false sensitiveHeaders: statistics-service: path: /statistics/** serviceId: statistics-service stripPrefix: false sensitiveHeaders: notification-service: path: /notifications/** serviceId: notification-service stripPrefix: false sensitiveHeaders: server: port: 4000 ================================================ FILE: config/src/main/resources/shared/monitoring.yml ================================================ ================================================ FILE: config/src/main/resources/shared/notification-service.yml ================================================ security: oauth2: client: clientId: notification-service clientSecret: ${NOTIFICATION_SERVICE_PASSWORD} accessTokenUri: http://auth-service:5000/uaa/oauth/token grant-type: client_credentials scope: server server: servlet: context-path: /notifications port: 8000 remind: cron: 0 0 0 * * * email: text: "Hey, {0}! We''ve missed you here on PiggyMetrics. It''s time to check your budget statistics.\r\n\r\nCheers,\r\nPiggyMetrics team" subject: PiggyMetrics reminder backup: cron: 0 0 12 * * * email: text: "Howdy, {0}. Your account backup is ready.\r\n\r\nCheers,\r\nPiggyMetrics team" subject: PiggyMetrics account backup attachment: backup.json spring: data: mongodb: host: notification-mongodb username: user password: ${MONGODB_PASSWORD} database: piggymetrics port: 27017 mail: host: smtp.gmail.com port: 465 username: dev-user password: dev-password properties: mail: smtp: auth: true socketFactory: port: 465 class: javax.net.ssl.SSLSocketFactory fallback: false ssl: enable: true ================================================ FILE: config/src/main/resources/shared/registry.yml ================================================ server: port: 8761 ================================================ FILE: config/src/main/resources/shared/statistics-service.yml ================================================ security: oauth2: client: clientId: statistics-service clientSecret: ${STATISTICS_SERVICE_PASSWORD} accessTokenUri: http://auth-service:5000/uaa/oauth/token grant-type: client_credentials scope: server spring: data: mongodb: host: statistics-mongodb username: user password: ${MONGODB_PASSWORD} database: piggymetrics port: 27017 server: servlet: context-path: /statistics port: 7000 rates: url: https://api.exchangeratesapi.io ================================================ FILE: config/src/main/resources/shared/turbine-stream-service.yml ================================================ ================================================ FILE: docker-compose.dev.yml ================================================ version: '2.1' services: rabbitmq: ports: - 5672:5672 config: build: config ports: - 8888:8888 registry: build: registry gateway: build: gateway auth-service: build: auth-service ports: - 5000:5000 auth-mongodb: build: mongodb ports: - 25000:27017 account-service: build: account-service ports: - 6000:6000 account-mongodb: build: mongodb ports: - 26000:27017 statistics-service: build: statistics-service ports: - 7000:7000 statistics-mongodb: build: mongodb ports: - 27000:27017 notification-service: build: notification-service ports: - 8000:8000 notification-mongodb: build: mongodb ports: - 28000:27017 monitoring: build: monitoring turbine-stream-service: build: turbine-stream-service ================================================ FILE: docker-compose.yml ================================================ version: '2.1' services: rabbitmq: image: rabbitmq:3-management restart: always ports: - 15672:15672 logging: options: max-size: "10m" max-file: "10" config: environment: CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD image: sqshq/piggymetrics-config restart: always logging: options: max-size: "10m" max-file: "10" registry: environment: CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD image: sqshq/piggymetrics-registry restart: always depends_on: config: condition: service_healthy ports: - 8761:8761 logging: options: max-size: "10m" max-file: "10" gateway: environment: CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD image: sqshq/piggymetrics-gateway restart: always depends_on: config: condition: service_healthy ports: - 80:4000 logging: options: max-size: "10m" max-file: "10" auth-service: environment: CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD NOTIFICATION_SERVICE_PASSWORD: $NOTIFICATION_SERVICE_PASSWORD STATISTICS_SERVICE_PASSWORD: $STATISTICS_SERVICE_PASSWORD ACCOUNT_SERVICE_PASSWORD: $ACCOUNT_SERVICE_PASSWORD MONGODB_PASSWORD: $MONGODB_PASSWORD image: sqshq/piggymetrics-auth-service restart: always depends_on: config: condition: service_healthy logging: options: max-size: "10m" max-file: "10" auth-mongodb: environment: MONGODB_PASSWORD: $MONGODB_PASSWORD image: sqshq/piggymetrics-mongodb restart: always logging: options: max-size: "10m" max-file: "10" account-service: environment: CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD ACCOUNT_SERVICE_PASSWORD: $ACCOUNT_SERVICE_PASSWORD MONGODB_PASSWORD: $MONGODB_PASSWORD image: sqshq/piggymetrics-account-service restart: always depends_on: config: condition: service_healthy logging: options: max-size: "10m" max-file: "10" account-mongodb: environment: INIT_DUMP: account-service-dump.js MONGODB_PASSWORD: $MONGODB_PASSWORD image: sqshq/piggymetrics-mongodb restart: always logging: options: max-size: "10m" max-file: "10" statistics-service: environment: CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD MONGODB_PASSWORD: $MONGODB_PASSWORD STATISTICS_SERVICE_PASSWORD: $STATISTICS_SERVICE_PASSWORD image: sqshq/piggymetrics-statistics-service restart: always depends_on: config: condition: service_healthy logging: options: max-size: "10m" max-file: "10" statistics-mongodb: environment: MONGODB_PASSWORD: $MONGODB_PASSWORD image: sqshq/piggymetrics-mongodb restart: always logging: options: max-size: "10m" max-file: "10" notification-service: environment: CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD MONGODB_PASSWORD: $MONGODB_PASSWORD NOTIFICATION_SERVICE_PASSWORD: $NOTIFICATION_SERVICE_PASSWORD image: sqshq/piggymetrics-notification-service restart: always depends_on: config: condition: service_healthy logging: options: max-size: "10m" max-file: "10" notification-mongodb: image: sqshq/piggymetrics-mongodb restart: always environment: MONGODB_PASSWORD: $MONGODB_PASSWORD logging: options: max-size: "10m" max-file: "10" monitoring: environment: CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD image: sqshq/piggymetrics-monitoring restart: always depends_on: config: condition: service_healthy ports: - 9000:8080 logging: options: max-size: "10m" max-file: "10" turbine-stream-service: environment: CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD image: sqshq/piggymetrics-turbine-stream-service restart: always depends_on: config: condition: service_healthy ports: - 8989:8989 logging: options: max-size: "10m" max-file: "10" ================================================ FILE: gateway/Dockerfile ================================================ FROM java:8-jre MAINTAINER Alexander Lukyanchikov ADD ./target/gateway.jar /app/ CMD ["java", "-Xmx200m", "-jar", "/app/gateway.jar"] EXPOSE 4000 ================================================ FILE: gateway/pom.xml ================================================ 4.0.0 gateway 1.0-SNAPSHOT jar gateway com.piggymetrics piggymetrics 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-netflix-zuul org.springframework.cloud spring-cloud-starter-config org.springframework.cloud spring-cloud-starter org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-sleuth org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ${project.name} ================================================ FILE: gateway/src/main/java/com/piggymetrics/gateway/GatewayApplication.java ================================================ package com.piggymetrics.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @SpringBootApplication @EnableDiscoveryClient @EnableZuulProxy public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } } ================================================ FILE: gateway/src/main/resources/bootstrap.yml ================================================ spring: application: name: gateway cloud: config: uri: http://config:8888 fail-fast: true password: ${CONFIG_SERVICE_PASSWORD} username: user ================================================ FILE: gateway/src/main/resources/static/attribution.html ================================================ Piggy Metrics
Creative Commons – Attribution (CC BY 3.0)
Thanks a lot for icons from The Noun Project collection.

Here's the list of all used icons:
Piggy Bank designed by Jezmael Basilio
Arrow designed by Jardson A.
Wallet designed by Luis Prado
Analytics designed by Aneeque Ahmed
Piggy Bank designed by Michelle Ann
Light Bulb designed by Chris Brunskill
Speech Bubble designed by Cengiz SARI
Bag designed by Agus Purwanto
Analytics designed by Luboš Volkov
College Tuition designed by Rediffusion
Marijuana designed by Gareth
Stroller designed by Edward Boatman
Television designed by Piero Borgo
Island designed by Bohdan Burmich
Light Bulb designed by Rémy Médard
Shirt designed by Megan Sheehan
Telephone designed by Ian Mawle
Shopping Cart designed by Megan Sheehan
Gas designed by Jon Testa
================================================ FILE: gateway/src/main/resources/static/css/animation.css ================================================ /* ZOOM AVATAR */ @-webkit-keyframes zoomavatar { 0% { -webkit-transform: scale(0.1); } 60% { -webkit-transform: scale(1.3); } 100% { -webkit-transform: scale(1); } } @-moz-keyframes zoomavatar { 0% { -moz-transform: scale(0.1); } 80% { -moz-transform: scale(1.3); } 100% { -moz-transform: scale(1); } } @-o-keyframes zoomavatar { 0% { -o-transform: scale(0.1); } 60% { -o-transform: scale(1.3); } 100% { -o-transform: scale(1); } } @-ms-keyframes zoomavatar { 0% { -ms-transform: scale(0.1); } 60% { -ms-transform: scale(1.3); } 100% { -ms-transform: scale(1); } } @keyframes zoomavatar { 0% { transform: scale(0.1); } 60% { transform: scale(1.3); } 100% { transform: scale(1); } } /* UNZOOM AVATAR */ @-webkit-keyframes unzoomavatar { 0% { -webkit-transform: scale(1); } 40% { -webkit-transform: scale(1.3); } 100% { -webkit-transform: scale(0.1); } } @-moz-keyframes unzoomavatar { 0% { -moz-transform: scale(1); } 40% { -moz-transform: scale(1.3); } 100% { -moz-transform: scale(0.1); } } @-o-keyframes unzoomavatar { 0% { -o-transform: scale(1); } 40% { -o-transform: scale(1.3); } 100% { -o-transform: scale(0.1); } } @-ms-keyframes unzoomavatar { 0% { -ms-transform: scale(1); } 40% { -ms-transform: scale(1.3); } 100% { -ms-transform: scale(0.1); } } @keyframes unzoomavatar { 0% { transform: scale(1); } 40% { transform: scale(1.3); } 100% { transform: scale(0.1); } } /* GO LEFT */ @-webkit-keyframes goleft { 0% { right: 100px; left:0%; } 60% { right: 300px; left:-20%; } 100% { right: 300px; left:-18%; } } @-moz-keyframes goleft { 0% { right: 100px; left:0%; } 60% { right: 300px; left:-20%; } 100% { right: 300px; left:-18%; } } @-o-keyframes goleft { 0% { right: 100px; left:0%; } 60% { right: 300px; left:-20%; } 100% { right: 300px; left:-18%; } } @-ms-keyframes goleft { 0% { right: 100px; left:0%; } 60% { right: 300px; left:-20%; } 100% { right: 300px; left:-18%; } } @keyframes goleft { 0% { right: 100px; left:0%; } 60% { right: 300px; left:-20%; } 100% { right: 300px; left:-18%; } } /* GO RIGHT */ @-webkit-keyframes goright { 0% { right: -100px; left:0%; } 60% { right: -300px; left:20%; } 100% { right: -300px; left:18%; } } @-moz-keyframes goright { 0% { right: -100px; left:0%; } 60% { right: -300px; left:20%; } 100% { right: -300px; left:18%; } } @-o-keyframes goright { 0% { right: -100px; left:0%; } 60% { right: -300px; left:20%; } 100% { right: -300px; left:18%; } } @-ms-keyframes goright { 0% { right: -100px; left:0%; } 60% { right: -300px; left:20%; } 100% { right: -300px; left:18%; } } @keyframes goright { 0% { right: -100px; left:0%; } 60% { right: -300px; left:20%; } 100% { right: -300px; left:18%; } } /* GO DOWN */ @-webkit-keyframes godown { 0% { bottom:20%; } 60% { bottom:4%; } 100% { bottom:8%; } } @-moz-keyframes godown { 0% { bottom:20%; } 60% { bottom:4%; } 100% { bottom:8%; } } @-o-keyframes godown { 0% { bottom:20%; } 60% { bottom:4%; } 100% { bottom:8%; } } @-ms-keyframes godown { 0% { bottom:20%; } 60% { bottom:4%; } 100% { bottom:8%; } } @keyframes godown { 0% { bottom:20%; } 60% { bottom:4%; } 100% { bottom:8%; } } /* PLUS */ @-webkit-keyframes plus { 0% { -webkit-transform: scale(0.85); } 8% { -webkit-transform: scale(1); } 80% { -webkit-transform: scale(0.85); } 100% { -webkit-transform: scale(0.85); } } @-moz-keyframes plus { 0% { -moz-transform: scale(0.85); } 8% { -moz-transform: scale(1); } 80% { -moz-transform: scale(0.85); } 100% { -moz-transform: scale(0.85); } } @-o-keyframes plus { 0% { -o-transform: scale(0.85); } 8% { -o-transform: scale(1); } 80% { -o-transform: scale(0.85); } 100% { -o-transform: scale(0.85); } } @-ms-keyframes plus { 0% { -ms-transform: scale(0.85); } 8% { -ms-transform: scale(1); } 80% { -ms-transform: scale(0.85); } 100% { -ms-transform: scale(0.85); } } @keyframes plus { 0% { transform: scale(0.85); } 8% { transform: scale(1); } 80% { transform: scale(0.85); } 100% { transform: scale(0.85); } } /* SLIDER */ @-webkit-keyframes endoflist { 0% { -webkit-transform: translateY(0px); } 20% { -webkit-transform: translateY(-60px); } 100% { -webkit-transform: translateY(0px); } } @-moz-keyframes endoflist { 0% { -moz-transform: translateY(0px); } 20% { -moz-transform: translateY(-60px); } 100% { -moz-transform: translateY(0px); } } @-o-keyframes endoflist { 0% { -o-transform: translateY(0px); } 20% { -o-transform: translateY(-60px); } 100% { -o-transform: translateY(0px); } } @-ms-keyframes endoflist { 0% { -ms-transform: translateY(0px); } 20% { -ms-transform: translateY(-60px); } 100% { -ms-transform: translateY(0px); } } @keyframes endoflist { 0% { transform: translateY(0px); } 20% { transform: translateY(-60px); } 100% { transform: translateY(0px); } } @-webkit-keyframes startoflist { 0% { -webkit-transform: translateY(0px); } 20% { -webkit-transform: translateY(60px); } 100% { -webkit-transform: translateY(0px); } } @-moz-keyframes startoflist { 0% { -moz-transform: translateY(0px); } 20% { -moz-transform: translateY(60px); } 100% { -moz-transform: translateY(0px); } } @-o-keyframes startoflist { 0% { -o-transform: translateY(0px); } 20% { -o-transform: translateY(60px); } 100% { -o-transform: translateY(0px); } } @-ms-keyframes startoflist { 0% { -ms-transform: translateY(0px); } 20% { -ms-transform: translateY(60px); } 100% { -ms-transform: translateY(0px); } } @keyframes startoflist { 0% { transform: translateY(0px); } 20% { transform: translateY(60px); } 100% { transform: translateY(0px); } } @-webkit-keyframes frameanimate { 0% { -webkit-transform: translateY(-284px); } 50% { -webkit-transform: translateY(50px); } 100% { -webkit-transform: translateY(0px); } } @-moz-keyframes frameanimate { 0% { -moz-transform: translateY(-284px); } 50% { -moz-transform: translateY(100px); } 100% { -moz-transform: translateY(0px); } } @-o-keyframes frameanimate { 0% { -o-transform: translateY(-284px); } 50% { -o-transform: translateY(100px); } 100% { -o-transform: translateY(0px); } } @-ms-keyframes frameanimate { 0% { -ms-transform: translateY(-284px); } 50% { -ms-transform: translateY(100px); } 100% { -ms-transform: translateY(0px); } } @keyframes frameanimate { 0% { transform: translateY(-284px); } 50% { transform: translateY(100px); } 100% { transform: translateY(0px); } } /*MODAL WINDOWS*/ /* MODAL FORWARD */ @-webkit-keyframes modalforward { 0% { -webkit-transform: scale(0.4); } 60% { -webkit-transform: scale(1.1); } 100% { -webkit-transform: scale(1); } } @-moz-keyframes modalforward { 0% { -moz-transform: scale(0.4); } 80% { -moz-transform: scale(1.1); } 100% { -moz-transform: scale(1); } } @-o-keyframes modalforward { 0% { -o-transform: scale(0.4); } 60% { -o-transform: scale(1.1); } 100% { -o-transform: scale(1); } } @-ms-keyframes modalforward { 0% { -ms-transform: scale(0.4); } 60% { -ms-transform: scale(1.1); } 100% { -ms-transform: scale(1); } } @keyframes modalforward { 0% { transform: scale(0.4); } 60% { transform: scale(1.1); } 100% { transform: scale(1); } } /* UNZOOM REVERSE */ @-webkit-keyframes modalreverse { 0% { -webkit-transform: scale(1); } 40% { -webkit-transform: scale(1.06); } 100% { -webkit-transform: scale(0.6); } } @-moz-keyframes modalreverse { 0% { -moz-transform: scale(1); } 40% { -moz-transform: scale(1.06); } 100% { -moz-transform: scale(0.6); } } @-o-keyframes modalreverse { 0% { -o-transform: scale(1); } 40% { -o-transform: scale(1.06); } 100% { -o-transform: scale(0.6); } } @-ms-keyframes modalreverse { 0% { -ms-transform: scale(1); } 40% { -ms-transform: scale(1.06); } 100% { -ms-transform: scale(0.6); } } @keyframes modalreverse { 0% { transform: scale(1); } 40% { transform: scale(1.06); } 100% { transform: scale(0.6); } } /* MODAL VALUE ERROR */ @-webkit-keyframes modalvalueerror { 0% { -webkit-transform: scale(1); } 50% { -webkit-transform: scale(1.5); } 100% { -webkit-transform: scale(1); } } @-moz-keyframes modalvalueerror { 0% { -moz-transform: scale(1); } 40% { -moz-transform: scale(1.5); } 100% { -moz-transform: scale(1); } } @-o-keyframes modalvalueerror { 0% { -o-transform: scale(1); } 40% { -o-transform: scale(1.5); } 100% { -o-transform: scale(1); } } @-ms-keyframes modalvalueerror { 0% { -ms-transform: scale(1); } 40% { -ms-transform: scale(1.5); } 100% { -ms-transform: scale(1); } } @keyframes modalvalueerror { 0% { transform: scale(1); } 40% { transform: scale(1.5); } 100% { transform: scale(1); } } /* NEW ITEM ADDED */ @-webkit-keyframes newitemadded { 20% { background-color: #f2f2f2; } 70% { background-color: #f2f2f2; } 100% { background-color: white; } } @-moz-keyframes newitemadded { 30% { background-color: #f2f2f2; } 70% { background-color: #f2f2f2; } 100% { background-color: white; } } @-o-keyframes newitemadded { 30% { background-color: #f2f2f2; } 70% { background-color: #f2f2f2; } 100% { background-color: white; } } @-ms-keyframes newitemadded { 30% { background-color: #f2f2f2; } 70% { background-color: #f2f2f2; } 100% { background-color: white; } } @keyframes newitemadded { 30% { background-color: #f2f2f2; } 70% { background-color: #f2f2f2; } 100% { background-color: white; } } /* BUBBLE */ @-webkit-keyframes bubble { 5% { opacity: 0; background-position: -280px 0; top: 21px; right: 10px; } 10% { opacity: 0; background-position: -240px 0; top: 12px; right: 12px; } 40% { opacity: 1; background-position: -240px 0; top: 12px; right: 12px; } 80% { opacity: 1; background-position: -240px 0; top: 12px; right: 12px; } 90% { opacity: 0; background-position: -240px 0; top: 12px; right: 12px; } 95% { opacity: 0; background-position: -280px 0; top: 21px; right: 10px; } 100% { opacity: 1; background-position: -280px 0; top: 21px; right: 10px; } } @-moz-keyframes bubble { 5% { opacity: 0; background-position: -280px 0; top: 21px; right: 10px; } 10% { opacity: 0; background-position: -240px 0; top: 12px; right: 12px; } 40% { opacity: 1; background-position: -240px 0; top: 12px; right: 12px; } 80% { opacity: 1; background-position: -240px 0; top: 12px; right: 12px; } 90% { opacity: 0; background-position: -240px 0; top: 12px; right: 12px; } 95% { opacity: 0; background-position: -280px 0; top: 21px; right: 10px; } 100% { opacity: 1; background-position: -280px 0; top: 21px; right: 10px; } } @keyframes bubble { 5% { opacity: 0; background-position: -280px 0; top: 21px; right: 10px; } 10% { opacity: 0; background-position: -240px 0; top: 12px; right: 12px; } 40% { opacity: 1; background-position: -240px 0; top: 12px; right: 12px; } 80% { opacity: 1; background-position: -240px 0; top: 12px; right: 12px; } 90% { opacity: 0; background-position: -240px 0; top: 12px; right: 12px; } 95% { opacity: 0; background-position: -280px 0; top: 21px; right: 10px; } 100% { opacity: 1; background-position: -280px 0; top: 21px; right: 10px; } } @-webkit-keyframes spincircle { 0% { -webkit-transform: rotate(0deg); } 95% { -webkit-transform: rotate(360deg); } 100% { -webkit-transform: rotate(370deg); } } @-moz-keyframes spincircle { 0% { -moz-transform: rotate(0deg); } 95% { -moz-transform: rotate(360deg); } 100% { -moz-transform: rotate(370deg); } } @-o-keyframes spincircle { 0% { -o-transform: rotate(0deg); } 95% { -o-transform: rotate(360deg); } 100% { -o-transform: rotate(370deg); } } @-ms-keyframes spincircle { 0% { -ms-transform: rotate(0deg); } 95% { -ms-transform: rotate(360deg); } 100% { -ms-transform: rotate(370deg); } } @keyframes spincircle { 0% { transform: rotate(0deg); } 95% { -transform: rotate(360deg); } 100% { transform: rotate(370deg); } } ================================================ FILE: gateway/src/main/resources/static/css/launch.css ================================================ #createaccount, #enter, #secondenter, #info, #backlogin, #backpassword, #plusavatar, .columnimage { background: url("../images/1pagesprites.png") no-repeat; background-size: 650px 197px; } @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) { #createaccount, #enter, #secondenter, #info, #backlogin, #backpassword, #plusavatar, .columnimage { background: url("../images/1pagesprites@2x.png") no-repeat; background-size: 650px 197px; } } /*========================== CARDS WRAPPER ===========================*/ #loginpage { top:0; bottom: 0; left:0; right: 0; position: absolute; margin: auto; width: 300px; height: 500px; -webkit-perspective: 1000px; -moz-perspective: 1000px; -ms-perspective: 1000px; -o-perspective: 1000px; perspective: 1000px; display: none; } .CCattribution { position: relative; top: 50px; text-align: center; font-family: "Helvetica", "Arial"; font-size: 12px; line-height: 30px; color: #525252; padding-bottom: 90px; } .CCattribution a { color: #7ba1b4; } #loginpage, .front, .back { width: 300px; height: 500px; } #flipper { -webkit-transform-style: preserve-3d; -moz-transform-style: preserve-3d; -ms-transform-style: preserve-3d; -o-transform-style: preserve-3d; transform-style: preserve-3d; -moz-backface-visibility: hidden; -webkit-transition: 1000ms; -moz-transition: 1000ms; -ms-transition: 1000ms; -o-transition: 1000ms; transition: 1000ms; position: relative; overflow: visible; } .flippedcardinfo { -webkit-transform: rotateY(180deg); -moz-transform: rotateY(180deg); -ms-transform: rotateY(180deg); -o-transform: rotateY(180deg); transform: rotateY(180deg); } .FRONTCARD, .BACKCARD { -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -ms-backface-visibility: hidden; -o-backface-visibility: hidden; backface-visibility: hidden; overflow: visible; top:0; bottom: 0; left:0; right: 0; position: absolute; text-align: center; width: 300px; } .fliptext { position: absolute; text-transform: uppercase; left: 0px; right: 0px; bottom: -10px; height: 30px; font-size: 11px; color: #6e6e6e; line-height: 0.95em; display: inline-block; font-family: Arial; background-color: white; } .fliptext a{ color: #96bcce; text-decoration: none; border-bottom: 1px dashed #96bcce; } .fliptext a:hover{ color: #7ba1b4; text-decoration: none; border-bottom: 1px dashed #7ba1b4; } /*================================= FIRST CARD =================================*/ .FRONTCARD { z-index: 2; height: 450px; background-color: white; -webkit-transform: rotateY(0deg); -moz-transform: rotateY(0deg); -ms-transform: rotateY(0deg); -o-transform: rotateY(0deg); transform: rotateY(0deg); } .auto-shake{ -webkit-animation: 1s auto-shake ease-out; -moz-animation: 1s auto-shake ease-out; -o-animation: 1s auto-shake ease-out; -ms-animation: 1s auto-shake ease-out; animation: 1s auto-shake ease-out; } .hover-shake{ -webkit-animation: 1.6s hover-shake ease-out; -moz-animation: 1.6s hover-shake ease-out; -o-animation: 1.6s hover-shake ease-out; -ms-animation: 1.6s hover-shake ease-out; animation: 1.6s hover-shake ease-out; } .loadingspin{ -webkit-animation: 2s loadingspin; -moz-animation: 2s loadingspin; -o-animation: 2s loadingspin; -ms-animation: 2s loadingspin; animation: 2s loadingspin; } #piggy { display: block; background: url("../images/piggy_large.gif") no-repeat; background-size: 178px 178px; width: 178px; height: 178px; position: relative; margin: auto; z-index: 50; } #enter, #secondenter { -webkit-animation: enterarrow 2s infinite; -moz-animation: enterarrow 2s infinite; -o-animation: enterarrow 2s infinite; -ms-animation: enterarrow 2s infinite; animation: enterarrow 2s infinite; background-position: -190px -60px; margin-left: 222px; margin-top: 18px; width: 52px; height: 12px; position: absolute; cursor: pointer; display: none; z-index: 7; } #secondenter{ position: absolute; bottom: 85px; z-index: 4; } #preloader{ background: url("../images/preloader.gif") center no-repeat; background-size: 18px 18px; margin-left: 210px; bottom: 69px; position: absolute; display: none; width: 52px; height: 46px; z-index: 5; } /*FLIP BUTTON*/ #wrapper { display: block; font-size: 13px; color: black; position: absolute; top: 335px; width: 300px; height: 46px; -webkit-perspective: 1100px; -moz-perspective: 1100px; -ms-perspective: 1100px; -o-perspective: 1100px; perspective: 1100px; -ms-transform: rotated3(1,0,0,0); -webkit-transform-origin: 50% 46px 0; -moz-transform-origin: 50% 46px 0; -ms-transform-origin: 50% 46px 0; -o-transform-origin: 50% 46px 0; transform-origin: 10% 46px 0; z-index: 3; } #cube { position: absolute; width: 300px; height: 46px; -webkit-transform-style: preserve-3d; -moz-transform-style: preserve-3d; -ms-transform-style: preserve-3d; -o-transform-style: preserve-3d; transform-style: preserve-3d; -webkit-transition: all 400ms cubic-bezier(0.67, 0.07, 0.50, 0.88); -moz-transition: all 400ms cubic-bezier(0.67, 0.07, 0.50, 0.88); -ms-transition: all 400ms cubic-bezier(0.67, 0.07, 0.50, 0.88); -o-transition: all 400ms cubic-bezier(0.67, 0.07, 0.50, 0.88); transition: all 400ms cubic-bezier(0.67, 0.07, 0.50, 0.88); } .flippedform{ -webkit-transform-origin: 0 46px -46px; -moz-transform-origin: 0 46px -46px; -ms-transform-origin: 0 46px -46px; -o-transform-origin: 0 46px -46px; transform-origin: 0 46px -46px; -webkit-transform: rotate3d(1,0,0,90deg); -moz-transform: rotate3d(1,0,0,90deg); -ms-transform: rotate3d(1,0,0,90deg); -o-transform: rotate3d(1,0,0,90deg); transform: rotate3d(1,0,0,90deg); } .side { outline: 3px double #a6ccd9; text-indent: 15px; width: 300px; height: 46px; line-height: 48px; position: absolute; } #backlogin, #backpassword { display: block; position: absolute; left: 66px; top: 12px; border-color: 1px solid; width: 22px; height: 20px; } #backlogin { background-position: -250px -24px; } #backpassword { background-position: -250px 0; } #side1 { background-position: -250px -24px; background-color: white; width: 300px; height: 46px; } #side2 { background-color: white; -webkit-transform: translateZ(-23px) translateY(23px) rotateX(-90deg); -moz-transform: translateZ(-23px) translateY(23px) rotateX(-90deg); -ms-transform: translateZ(-23px) translateY(23px) rotateX(-90deg); -o-transform: translateZ(-23px) translateY(23px) rotateX(-90deg); transform: translateZ(-23px) translateY(23px) rotateX(-90deg); } /* END OF FLIP BUTTON */ #logotext { background: url("../images/logotext_large.gif") no-repeat; background-size: 172px 65px; width: 172px; height: 65px; position: relative; margin: auto; margin-top: 36px; } #info { background-position: -190px 0; cursor: pointer; width: 49px; height: 49px; position: absolute; margin: auto; top:-10px; right: -20px; } /* FIRST CARD FORMS */ input[class="frontforms"]{ border: 0px solid; position: relative; color: #4c4c4c; font-family: Arial; font-size: 15px; } input[id="frontloginform"]{ margin-left: 20px; height: 25px; width: 110px; } input[id="frontpasswordform"]{ margin-left: 25px; height: 25px; width: 95px; } .ghostform { display: none; } input[class="backforms"]:focus { outline: none; } input[class="backforms"]::-webkit-input-placeholder{font-family: Arial; font-size: 15px; color: #cdcdcd;} input[class="backforms"]:-ms-input-placeholder{font-family: Arial; font-size: 15px; color: #cdcdcd;} input[class="backforms"]:-moz-placeholder{font-family: Arial; font-size: 15px; color: #cdcdcd;} input[type="text"]:focus, input[type="password"]:focus { outline: none; } input[class="frontforms"]::-webkit-input-placeholder{font-family: Arial; font-size: 15px; color: #cdcdcd;} input[class="frontforms"]:-ms-input-placeholder{font-family: Arial; font-size: 15px; color: #cdcdcd;} input[class="frontforms"]:-moz-placeholder{font-family: Arial; font-size: 15px; color: #cdcdcd;} /*================================= SECOND CARD =================================*/ .BACKCARD { -webkit-transform: rotateY(180deg); -moz-transform: rotateY(180deg); -ms-transform: rotateY(180deg); -o-transform: rotateY(180deg); transform: rotateY(180deg); background-color: white; z-index: 1; height: 500px; } #franklin{ border: 1px solid #b9d6dc; -webkit-border-radius: 90px; -ms-border-radius: 90px; -o-border-radius: 90px; -moz-border-radius: 90px; border-radius: 90px; top:0; left: 0; right: 0; bottom: 0; position: relative; margin: auto; width: 162px; height: 162px; } #plusavatar { -webkit-transition: 0.4s; -moz-transition: 0.4s; -o-transition: 0.4s; -ms-transition: 0.4s; transition: 0.4s; opacity: 0; filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0); width: 180px; height: 180px; position: absolute; top:10px; left:0; right: 0; background-position: 0 -40px; margin: auto; z-index: 2; } .avataranimation { -webkit-animation: 2.5s plusavatar; -moz-animation: 2.5s plusavatar; -o-animation: 2.5s plusavatar; -ms-animation: 2.5s plusavatar; animation: 2.5s plusavatar; } .infiniteavataranimation { -webkit-animation: 1.5s infinite plusavatar; -moz-animation: 1.5s infinite plusavatar; -o-animation: 1.5s infinite plusavatar; -ms-animation: 1.5s infinite plusavatar; animation: 1.5s infinite plusavatar; } #createaccount { background-position: -190px -80px; width: 134px; height: 44px; position: relative; margin: auto; margin-top: 24px; } .inputWrapper { top:-335px; left: 0; right: 0; bottom: 0; margin: auto; overflow: hidden; position: absolute; cursor: pointer; width: 180px; height: 180px; z-index: 3; } .hidden { opacity: 0; -moz-opacity: 0; filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0) } .upload { -webkit-animation: plus 2s infinite; -moz-animation: plus 2s infinite; -o-animation: plus 2s infinite; -ms-animation: plus 2s infinite; animation: plus 2s infinite; } #mailform { position: absolute; top: 190px; margin: auto; right:-120px; text-transform: uppercase; width: 540px; height: 330px; font-size: 13px; display: block; opacity: 0.9; display: none; } .mailforminfo { position: relative; text-transform: none; top:-60px; font-family: "Museo-500"; font-size: 12px; color: #525252; } .mailforminfosmall { font-size: 11px; position: relative; top: 30px; line-height: 1.7; } #skipmail a { position: relative; text-transform: uppercase; left: 0px; right: 0px; bottom: -193px; font-size: 11px; font-family: Arial; color: #96bcce; text-decoration: none; border-bottom: 1px dashed #96bcce; } #skipmail a:hover{ color: #7ba1b4; text-decoration: none; border-bottom: 1px dashed #7ba1b4; } /* SECOND CARD FORMS */ #registrationforms { position: relative; top: 36px; line-height: 3.3em; } input[class="backforms"]{ text-align: center; border: 1px solid #a6ccd9; color: #4c4c4c; font-family: Arial; font-size: 15px; height: 30px; width: 296px; } input[id="backmailform"] { position: relative; text-align: center; border: 1px solid #a6ccd9; color: #4c4c4c; font-family: Arial; font-size: 15px; height: 30px; width: 296px; top: 140px; } .regbutton, .demobutton, .mailbutton { cursor: pointer; position: relative; background-color: white; border: 0px solid; outline: 3px double #a6ccd9; text-transform: uppercase; text-shadow: 1px 1px 1px white; color: #6e6e6e; } .regbutton:hover, .demobutton:hover, .mailbutton:hover { background-color: #f4f9fb; } .regbutton:active, .demobutton:active, .mailbutton:active { background-color: #ecf3f6; } .regbutton { height: 34px; width: 195px; top:18px; font-size: 11px; } .demobutton { letter-spacing: 3px; height: 50px; width: 300px; top: 436px; font-size: 12px; } .mailbutton { position: relative; height: 34px; width: 195px; top: 178px; font-size: 11px; } /* THIRD CARD */ .flipinfo, .frominfo { cursor: pointer; } .infoflipback { position: absolute; top: -12%; width: 100%; height: 500px; cursor: pointer; } #infopage { margin-left: -290px; margin-top: -90px; height: 660px; width: 870px; background-color: white; z-index: 4; display: none; position: absolute; color: #525252; } #regpage { display: block; } #infosubtitle, #infotitle, #infofooter, #iconsfooter { cursor: pointer; font-family: "Museo-500"; position: absolute; left: 0; right: 0; margin: auto; width: 100%; } #infosubtitle { text-transform: uppercase; font-size: 11px; top: 0; height: 30px; } #infotitle { text-transform: uppercase; font-size: 27px; top: 35px; height: 40px; } a#infofooter { background-image: url("../images/github.gif"); background-size: 25px 25px; background-repeat: no-repeat; background-position: center; color: #757575; text-decoration: none; display: block; line-height: 120px; font-size: 12px; bottom: 30px; margin: auto; width: 20%; height: 25px; } a#iconsfooter { background-position: center; color: #757575; text-decoration: none; font-size: 10px; bottom: -30px; } #infoline { position: relative; width: 610px; display: block; margin: auto; top: 425px; border-top: 1px solid #b8d9e1; } #infolinetext { font-family: "Museo-500"; text-transform: uppercase; letter-spacing: 2px; font-size: 10px; line-height: 30px; position: relative; width: 245px; height: 30px; display: block; margin: auto; top: 410px; background-color: white; } .infocolumn { position: absolute; top: 150px; margin: auto; display: block; width: 286px; height: 236px; overflow: hidden; } #infoleft { left:0; } #inforight { right:0; } #infocenter { left:0; right:0; } .columnimage { width: 104px; height: 104px; position: absolute; margin: auto; left:0; right: 0; top: 30px; } #leftcolumn > .columnimage { background-position: -546px 0; } #rightcolumn > .columnimage { background-position: -330px 0; } #centercolumn > .columnimage { background-position: -436px 0; } #leftcolumn, #rightcolumn, #centercolumn { -moz-transition: 0.8s; -o-transition: 0.8s ease-out; -ms-transition: 0.8s ease-out; -webkit-transition: 0.8s; transition: 0.8s; font-family: "Museo-500"; font-size: 12px; width: 100%; height: 200%; position: absolute; padding-top: 157px; text-align: center; top: 0%; } #leftcolumn:hover, #rightcolumn:hover, #centercolumn:hover { top:-100%; } .columnfooter { position: relative; top: 150px; font-size: 11px; line-height: 17px; } @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) { #piggy { background-image: url("../images/piggy_large@2x.gif"); } #logotext { background-image: url("../images/logotext_large@2x.gif"); } } /* ANIMATION */ /* SPIN THAT SHIT */ @-webkit-keyframes auto-shake { 0% { -webkit-transform: rotate(0deg); } 35% { -webkit-transform: rotate(55deg); } 50% { -webkit-transform: rotate(-35deg); } 70% { -webkit-transform: rotate(20deg); } 90% { -webkit-transform: rotate(-10deg); } 100% { -webkit-transform: rotate(0deg); } } @-moz-keyframes auto-shake { 0% { -moz-transform: rotate(0deg); } 35% { -moz-transform: rotate(55deg); } 50% { -moz-transform: rotate(-35deg); } 70% { -moz-transform: rotate(20deg); } 90% { -moz-transform: rotate(-10deg); } 100% { -moz-transform: rotate(0deg); } } @-o-keyframes auto-shake { 0% { -o-transform: rotate(0deg); } 35% { -o-transform: rotate(55deg); } 50% { -o-transform: rotate(-35deg); } 70% { -o-transform: rotate(20deg); } 90% { -o-transform: rotate(-10deg); } 100% { -o-transform: rotate(0deg); } } @-ms-keyframes auto-shake { 0% { -ms-transform: rotate(0deg); } 35% { -ms-transform: rotate(55deg); } 50% { -ms-transform: rotate(-35deg); } 70% { -ms-transform: rotate(20deg); } 90% { -ms-transform: rotate(-10deg); } 100% { -ms-transform: rotate(0deg); } } @keyframes auto-shake { 0% { transform: rotate(0deg); } 35% { transform: rotate(55deg); } 50% { transform: rotate(-35deg); } 70% { transform: rotate(20deg); } 90% { transform: rotate(-10deg); } 100% { transform: rotate(0deg); } } /* SPIN AGAIN */ @-webkit-keyframes hover-shake { 0% { -webkit-transform: rotate(0deg); } 30% { -webkit-transform: rotate(-30deg); } 50% { -webkit-transform: rotate(20deg); } 70% { -webkit-transform: rotate(-10deg); } 90% { -webkit-transform: rotate(5deg); } 100% { -webkit-transform: rotate(0deg); } } @-moz-keyframes hover-shake { 0% { -moz-transform: rotate(0deg); } 30% { -moz-transform: rotate(-30deg); } 50% { -moz-transform: rotate(20deg); } 70% { -moz-transform: rotate(-10deg); } 90% { -moz-transform: rotate(5deg); } 100% { -moz-transform: rotate(0deg); } } @-o-keyframes hover-shake { 0% { -o-transform: rotate(0deg); } 30% { -o-transform: rotate(-30deg); } 50% { -o-transform: rotate(20deg); } 70% { -o-transform: rotate(-10deg); } 90% { -o-transform: rotate(5deg); } 100% { -o-transform: rotate(0deg); } } @-ms-keyframes hover-shake { 0% { -ms-transform: rotate(0deg); } 30% { -ms-transform: rotate(-30deg); } 50% { -ms-transform: rotate(20deg); } 70% { -ms-transform: rotate(-10deg); } 90% { -ms-transform: rotate(5deg); } 100% { -ms-transform: rotate(0deg); } } @keyframes hover-shake { 0% { transform: rotate(0deg); } 30% { ransform: rotate(-30deg); } 50% { transform: rotate(20deg); } 70% { transform: rotate(-10deg); } 90% { transform: rotate(5deg); } 100% { transform: rotate(0deg); } } /*LOADING SPIN*/ @-webkit-keyframes loadingspin { 0% { -webkit-transform: rotate(0deg); } 20% { -webkit-transform: rotate(70deg); } 70% { -webkit-transform: rotate(-400deg); } 100% { -webkit-transform: rotate(-360deg); } } @-moz-keyframes loadingspin { 0% { -moz-transform: rotate(0deg); } 20% { -moz-transform: rotate(70deg); } 70% { -moz-transform: rotate(-400deg); } 100% { -moz-transform: rotate(-360deg); } } @-o-keyframes loadingspin { 0% { -o-transform: rotate(0deg); } 20% { -o-transform: rotate(70deg); } 70% { -o-transform: rotate(-400deg); } 100% { -o-transform: rotate(-360deg); } } @-ms-keyframes loadingspin { 0% { -ms-transform: rotate(0deg); } 20% { -ms-transform: rotate(70deg); } 70% { -ms-transform: rotate(-400deg); } 100% { -ms-transform: rotate(-360deg); } } @keyframes loadingspin { 0% { transform: rotate(0deg); } 20% { transform: rotate(70deg); } 70% { transform: rotate(-400deg); } 100% { transform: rotate(-360deg); } } /* ENTER ARROW */ @-webkit-keyframes enterarrow { 0% { left: 0px; } 5% { left: -5px; } 15% { left: 0px; } 20% { left: -5px; } 35% { left: 0px; } 100% { left: 0px; } } @-moz-keyframes enterarrow { 0% { left: 0px; } 5% { left: -5px; } 15% { left: 0px; } 20% { left: -5px; } 35% { left: 0px; } 100% { left: 0px; } } @-o-keyframes enterarrow { 0% { left: 0px; } 5% { left: -5px; } 15% { left: 0px; } 20% { left: -5px; } 35% { left: 0px; } 100% { left: 0px; } } @-ms-keyframes enterarrow { 0% { left: 0px; } 5% { left: -5px; } 15% { left: 0px; } 20% { left: -5px; } 35% { left: 0px; } 100% { left: 0px; } } @keyframes enterarrow { 0% { left: 0px; } 5% { left: -5px; } 15% { left: 0px; } 20% { left: -5px; } 35% { left: 0px; } 100% { left: 0px; } } /*PLUS AVATAR*/ @-webkit-keyframes plusavatar { 0% { opacity: 0; } 30% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } } @-moz-keyframes plusavatar { 0% { opacity: 0; } 60% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } } @-o-keyframes plusavatar { 0% { opacity: 0; } 60% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } } @-ms-keyframes plusavatar { 0% { opacity: 0; } 60% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } } @keyframes plusavatar { 0% { opacity: 0; } 60% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } } ================================================ FILE: gateway/src/main/resources/static/css/style.css ================================================ @font-face { font-family: 'Museo-100'; src: url('../fonts/museo-100/museo-100.eot'); src: local('@%@'), url('../fonts/museo-100/museo-100.eot?#iefix') format('embedded-opentype'), url('../fonts/museo-100/museo-100.woff') format('woff'), url('../fonts/museo-100/museo-100.svg') format('svg'), url('../fonts/museo-100/museo-100.ttf') format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'Museo-300'; src: url('../fonts/museo-300/museo-300.eot'); src: local('@%@'), url('../fonts/museo-300/museo-300.eot?#iefix') format('embedded-opentype'), url('../fonts/museo-300/museo-300.woff') format('woff'), url('../fonts/museo-300/museo-300.svg') format('svg'), url('../fonts/museo-300/museo-300.ttf') format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'Museo-500'; src: url('../fonts/museo-500/museo-500.eot'); src: local('@%@'), url('../fonts/museo-500/museo-500.eot?#iefix') format('embedded-opentype'), url('../fonts/museo-500/museo-500.woff') format('woff'), url('../fonts/museo-500/museo-500.svg') format('svg'), url('../fonts/museo-500/museo-500.ttf') format('truetype'); font-weight: normal; font-style: normal; } body { font-family: "Museo-300", Arial; -webkit-font-smoothing: antialiased; margin: auto; width: 100%; height: 100%; -webkit-user-select: none; -moz-user-select: none; -o-user-select: none; -ms-user-select: none; overflow: hidden; } .toppage, .bottompage { -webkit-transition: all 0.6s cubic-bezier(0.94, 0.06, 0.05, 0.95); -moz-transition: all0.6s cubic-bezier(0.77, 0, 0.175, 1); -ms-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1); -o-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1); transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1); position: absolute; display: block; width: 100%; height: 100%; } .toppage { top:0; width: 100%; height: 100%; display: block; } .bottompage { top:100%; width: 100%; height: 100%; display: none; z-index: 600; } .sectionup { -webkit-transform: translateY(100%); -moz-transform: translateY(100%); -o-transform: translateY(100%); -ms-transform: translateY(100%); transform: translateY(100%); } .sectionDown { -webkit-transform: translateY(-100%); -moz-transform: translateY(-100%); -o-transform: translateY(-100%); -ms-transform: translateY(-100%); transform: translateY(-100%); } input::-ms-clear { display: none; } input[type="text"], input[type="password"], textarea { -webkit-appearance: none; box-shadow: none; -webkit-box-shadow: none; -webkit-border-radius: 0px; -moz-border-radius: 0px; border-radius: 0px; } ::-moz-selection { background: rgb(255, 221, 45); } ::selection { background: rgb(255, 221, 45); } *:focus {outline:0px none transparent;} #bubble, #indicator, #save, #piggyicon, #rublesign, .arrowup, .arrowdown, .incomes-sprite-title, .close-sign, .modal-save-icon, #modaldeletecross, .initicons-arrow, .expenses-sprite-title, .savings-sprite-title, .triangle, .noUi-handle, .noUi-handle:after { background-size: 538px 84px; } #chooseicon, .itembackground, .iconbox, .itemlinebackground, #circle-select-1-back, #circle-select-2-back, #circle-select-3-back { background-image: url("../images/icons.png"); background-size: 1031px 42px; width: 42px; height: 42px; } @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) { #chooseicon, .itembackground, .iconbox, .itemlinebackground, #circle-select-1-back, #circle-select-2-back, #circle-select-3-back { background-image: url("../images/icons@2x.png"); } } .selectbox .select { width: 55px; height: 24px; padding: 0 45px 0 10px; font: 12px/24px Arial; border: 1px solid #ccc; } .selectbox .dropdown { top: 25px; width: 110px; margin: 0; padding: 0 0; background: #FFF; border: 1px solid #ccc; font: 12px Arial; } .selectbox .select .text { display: block; width: 100%; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .selectbox { color: #333; vertical-align: middle; cursor: pointer; margin-bottom: 26px; } .selectbox .trigger .arrow { position: absolute; top: 11px; right: 10px; border-left: 3px solid transparent; border-right: 3px solid transparent; border-top: 3px solid #a1a1a1; width: 0; height: 0; overflow: hidden; } .selectbox .select:active { background: #f8f8f8; } .selectbox li { padding: 5px 10px 6px; } .selectbox li.selected { background: #f6f6f6; } .selectbox li:hover { background: #f6f6f6; } #lastlogo { -webkit-perspective: 1000; -moz-perspective: 1000px; -ms-perspective: 1000px; -o-perspective: 1000px; perspective: 1000; position: absolute; top: 8%; left: 0; right: 0; margin: auto; z-index: 1000; width: 172px; height: 204px; display: none; } #lastlogoflipper { -webkit-transform-style: preserve-3d; -moz-transform-style: preserve-3d; -ms-transform-style: preserve-3d; -o-transform-style: preserve-3d; transform-style: preserve-3d; -moz-backface-visibility: hidden; -webkit-transition: 0.4s; -moz-transition: 0.4s; -ms-transition: 0.4s; -o-transition: 0.4s; transition: 0.4s; position: relative; overflow: visible; } #logo_greeting, #logo_settings { -moz-transition: 0.3s ease-out; -o-transition: 0.3s ease-out; -ms-transition: 0.3s ease-out; -webkit-transition: 0.3s ease-out; transition: 0.3s ease-out; background: url("../images/logo_large.gif") no-repeat; background-size: 137px 204px; background-position: center; width: 172px; height: 204px; position: absolute; margin: auto; left: 0; right: 0; display: none; z-index: 600; } #logo_greeting { position: absolute; display: none; top: 8%; } #logo_settings, #logo_statistic { -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -ms-backface-visibility: hidden; -o-backface-visibility: hidden; backface-visibility: hidden; overflow: visible; position: absolute; cursor: pointer; } #logo_statistic { -webkit-transform: rotateY(180deg); -moz-transform: rotateY(180deg); -ms-transform: rotateY(180deg); -o-transform: rotateY(180deg); transform: rotateY(180deg); background: url("../images/logotext_large.gif") no-repeat; background-size: 172px 65px; width: 172px; height: 65px; position: absolute; cursor: pointer; z-index: 3000; } #settings_hat { position: absolute; width: 100%; height: 36%; background-color: white; display: none; z-index: 500; } #logoclickplace { width: 100%; height: 100%; } #bubble { position: absolute; top:-20px; right: -60px; background-position: -180px 0; width: 57px; height: 57px; cursor: pointer; display: none; } #indicator { position: absolute; background-position: -280px 0; top: 21px; right: 10px; width: 31px; height: 31px; } .bubble-animation { -webkit-animation: 1s bubble ease-out; -moz-animation: 1s bubble ease-out; -o-animation: 1s bubble ease-out; -ms-animation: 1s bubble ease-out; animation: 1s bubble ease-out; } input { -webkit-font-smoothing: antialiased; } #centerbox { top:0; left: 0; right: 0; bottom: 0; position: absolute; width: 250px; height: 250px; margin: auto; display: none; } #avatarcontainer { font-family: "Museo-100"; border: 1px solid #b9d6dc; -webkit-border-radius: 90px; -moz-border-radius: 90px; border-radius: 90px; top:0; left: 0; right: 0; bottom: 0; position: absolute; cursor: pointer; width: 162px; height: 162px; margin: auto; } .forward { -webkit-animation: 0.4s zoomavatar ease-out; -moz-animation: 0.4s zoomavatar ease-out; -o-animation: 0.4s zoomavatar ease-out; -ms-animation: 0.4s zoomavatar ease-out; animation: 0.4s zoomavatar ease-out; } .reverse { -webkit-animation: 0.5s unzoomavatar ease-out; -moz-animation: 0.6s unzoomavatar ease-out; -o-animation: 0.4s unzoomavatar ease-out; -ms-animation: 0.4s unzoomavatar ease-out; animation: 0.5s unzoomavatar ease-out; } .avatar { background: url("../images/userpic.jpg") center center no-repeat; position: absolute; box-shadow:0px 0px 0px 5px white inset; width: 100%; height: 100%; -moz-border-radius: 90px; -webkit-border-radius: 90px; border-radius: 90px; background-size: 100% 100%; } #lefttitle, #righttitle { text-transform: uppercase; font-family: "Museo-300", Arial; font-size: 22px; position: absolute; text-align: center; display: none; width: 250px; height: 20px; margin: auto; } #lefttitle { -webkit-animation: 0.6s goleft ease-out; -moz-animation: 0.6s goleft ease-out; -o-animation: 0.6s goleft ease-out; -ms-animation: 0.6s goleft ease-out; animation: 0.6s goleft ease-out; top:0; left: -18%; right: 300px; bottom: 0; } #righttitle { -webkit-animation: 0.6s goright ease-out; -moz-animation: 0.6s goright ease-out; -o-animation: 0.6s goright ease-out; -ms-animation: 0.6s goright ease-out; animation: 0.6s goright ease-out; top:0; left: 18%; right: -300px; bottom: 0; } #bottombuttons { font-family: "Museo-500"; -webkit-animation: 0.6s godown ease-out; -moz-animation: 0.6s godown ease-out; -o-animation: 0.6s godown ease-out; -ms-animation: 0.6s godown ease-out; animation: 0.6s godown ease-out; position: absolute; left: 0; right: 0; bottom: 8%; margin: auto; display: none; width: 90px; height: 25%; } #plus { text-align: center; text-transform: uppercase; font-size: 11px; color: #a6cbd4; position: absolute; cursor: pointer; margin: auto; width: 90px; height: 90px; } #plus:hover { color: #9cbac2; } #plusborder { -webkit-animation: plus 2s infinite; -moz-animation: plus 2s infinite; -o-animation: plus 2s infinite; -ms-animation: plus 2s infinite; animation: plus 2s infinite; position: absolute; border: 2px solid #cfe6ec; -webkit-border-radius: 70px; -moz-border-radius: 70px; border-radius: 70px; top:0; left: 0; right: 0; bottom: 0; margin: auto; width: 80%; height: 80%; } #plusone, #plustwo { position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: auto; background-color: #cfe6ec; } #plusone { width: 2px; height: 50%; } #plustwo { width: 50%; height: 2px; } #plustext { font-family: Arial; position: absolute; left: 0; right: 0; bottom: -15px; margin: auto; } #minus { top: 70%; left: 0; right: 0; margin: auto; text-transform: uppercase; font-size: 10px; color: #c8c2c4; position: absolute; cursor: pointer; width: 35px; height: 35px; } #minus:hover { color: #a39b9d; } #minusborder { position: absolute; border: 1px solid #c8c2c4; -webkit-border-radius: 70px; -moz-border-radius: 70px; border-radius: 70px; top:0; left: 0; right: 0; bottom: 0; margin: auto; width: 80%; height: 80%; } #minusone { position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: auto; background-color: #c8c2c4; width: 50%; height: 1px; } #minustext { width: 70px; text-align: center; font-family: Arial; left: -18px; right: 0; bottom: -17px; margin: auto; position: absolute; } .bluetext { color: #a6d1d6; } #settingspage { display: none; } #swipefield { position: absolute; width: 100%; height: 100%; z-index: 0; } /* =============== S L I D E R S =============== */ .incomes-sprite-title, .expenses-sprite-title, .savings-sprite-title { background-size: 538px 84px; position: absolute; cursor: pointer; top:-4px; } .incomes-sprite-title { background-position: 0 -60px; width: 108px; height: 24px; left: 35%; cursor: pointer; } .expenses-sprite-title { background-position: -114px -60px; width: 114px; height: 24px; left: 35%; cursor: pointer; } .savings-sprite-title { background-position: -233px -60px; width: 164px; height: 24px; left: 30%; } .zoomplus:hover ~ .plusitem { -webkit-transform: scale(1.15); -moz-transform: scale(1.15); -o-transform: scale(1.15); -ms-transform: scale(1.15); transform: scale(1.15); } .zoomplus:active ~ .plusitem { -webkit-transform: scale(1); -moz-transform: scale(1); -o-transform: scale(1); -ms-transform: scale(1); transform: scale(1); } .blueline { background-color: #b8d9e1; position: absolute; top:47px; width: 100%; height: 1px; } .brownline { background-color: #a6989c; position: absolute; top:47px; width: 12px; height: 1px; } .columns { text-transform: uppercase; position: absolute; display: block; margin: auto; height: 390px; } #savebutton { position: absolute; left: 0; right: 0; display: block; cursor: pointer; width: 100%; height: 60px; bottom: 2%; overflow: hidden; } #savebutton:hover > #leftborder, #savebutton:hover > #rightborder, #savebutton:hover > #centerborder { background-color: #fafcfc; } #savebutton:active > #leftborder, #savebutton:active > #rightborder, #savebutton:active > #centerborder { background-color: #f3f7f8; } #leftborder, #rightborder, #centerborder { border-top: 1px solid #b8d9e1; position: absolute; margin:auto; height: 100%; display: block; -moz-transition: 0.2s; -o-transition: 0.2s; -ms-transition: 0.2s; -webkit-transition: 0.2s; transition: 0.2s; } .saveaction { background-color: #eff6f8; } #leftborder { left: -21%; right: 490px; width: 340px; } #rightborder { left: 21%; right: -490px; width: 340px; } #centerborder { left: 0; right: 0; width: 40%; } #save { margin: auto; position: absolute; bottom:15px; left: 25px; right: 0; width: 138px; height: 24px; background-position: -400px -60px; } /* =============== SAVINGS SLIDER =============== */ #piggyicon { background-position: -100px 0; background-size: 538px 84px; position: absolute; display: block; width: 46px; height: 38px; left: 25px; top: 70px; } #topsavingsline { position: relative; display: block; width: 100%; height: 190px; border-bottom: 1px solid #e2e2e2; } #bottomsavingsline { position: relative; display: block; width: 100%; height: 92px; border-bottom: 1px solid #e2e2e2; } #savingsvalue{ display: inline-block; border: 0px solid; background-color: transparent; position: absolute; top: 125px; left: 30%; font-family: "Museo-100"; font-size: 38px; padding: 0; } #rublesign { cursor: pointer; background-size: 538px 84px; display: inline-block; width: 22px; height: 31px; position: relative; z-index: 1000; } #rublebox { top: 135px; width: 30%; height: 40px; left: 0; position: relative; display: inline-block; } #percentvalue{ display: inline-block; border: 0px solid; position: relative; top: 48px; left: 125px; font-family: "Museo-300"; font-size: 22px; color: #9e9e9e; background-color: white; } /* TOGGLES */ #deposit + label.button{ position: absolute; top:16px; } #capitalization + label.button{ position: absolute; bottom:-42px; } input#deposit, input#capitalization { max-height: 0; max-width: 0; display: none; } input#deposit + label.button, input#capitalization + label.button{ display: block; position: absolute; right:32px; box-shadow: inset 0 0 0px 1px #e3e3e3; height: 21px; width: 33px; border-radius: 15px; } input#deposit + label.button:before, input#capitalization + label.button:before { content: ""; position: absolute; display: block; height: 21px; width: 21px; top: 0; left: 0; border-radius: 15px; -moz-transition: 0.2s ease-out; -o-transition: 0.2s ease-out; -ms-transition: 0.2s ease-out; -webkit-transition: 0.2s ease-out; transition: 0.2s ease-out; } input#deposit + label.button:after, input#capitalization + label.button:after { content: ""; position: absolute; display: block; height: 21px; width: 21px; top: 0; left: 0px; border-radius: 15px; background: white; box-shadow: inset 0 0 0 1px #e0e0e0, 1px 1px 2px #ebebeb; -moz-transition: 0.2s ease-out; -o-transition: 0.2s ease-out; -ms-transition: 0.2s ease-out; -webkit-transition: 0.2s ease-out; transition: 0.2s ease-out; } /* END OF TOGGLES */ input#deposit:checked + label.button:before, input#capitalization:checked + label.button:before { width: 33px; height: 21px; background: #e7f6f8; box-shadow: inset 0 0 0px 1px #b4d6da; } input#deposit:checked + label.button:after, input#capitalization:checked + label.button:after { left: 13px; box-shadow: inset 0 0 0 1px #b4d6da, 1px 1px 1px #ebebeb; } #deposit:checked ~ .savingscapital, #deposit:checked ~ .savingspercent, #deposit:checked ~ input#percentvalue { color: #4b4b4b; } .savingstitle14 { font-family: "Museo-300"; font-size: 14px; color: #4b4b4b; position: absolute; left: 30%; top: 70px; } .savingstitle12 { font-family: "Museo-300"; font-size: 13px; color: #a2c1c6; font-style: italic; position: absolute; left: 30%; top: 94px; } .savingsdeposit { font-family: "Museo-300"; font-size: 12px; color: #4b4b4b; position: absolute; left: 25px; top:20px; } .savingscapital, .savingspercent { font-family: "Museo-300"; font-size: 12px; color: #b5b5b5; position: absolute; } .savingscapital { left: 25px; bottom:-40px; } .savingspercent { left: 25px; bottom:20px; } /* =============== INCOMES SLIDER =============== */ #noincomes, #noexpenses { width: 160px; height: 160px; position: absolute; display: block; top: 30%; left: 0; right: 0; margin: auto; cursor: pointer; z-index: 10; } .hoverplace { display: block; width: 100%; height: 100%; } .hoverplace:hover ~ .plusitemtitle, .majorplusitem:hover ~ .plusitemtitle, .plusitemtitle:hover { color: #a2c1c6; } .majorplusitem { top:0; bottom:0; left: 0; right: 0; margin: auto; position: absolute; display: block; width: 75px; height: 75px; -webkit-transition: 150ms; -moz-transition: 150ms; -o-transition: 150ms; -ms-transition: 150ms; transition: 150ms; } .majorplusitem:hover { -webkit-transform: scale(1.1); -moz-transform: scale(1.1); -o-transform: scale(1.1); -ms-transform: scale(1.1); transform: scale(1.1); } .majorplusitem:active { -webkit-transform: scale(1); -moz-transform: scale(1); -o-transform: scale(1); -ms-transform: scale(1); transform: scale(1); } .plusitemtitle { text-align: center; width: 100%; font-family: "Museo-300"; font-size: 14px; color: #4b4b4b; position: absolute; bottom: 0; -webkit-transition: 0.25s; -moz-transition: 0.25s; -o-transition: 0.25s; -ms-transition: 0.25s; transition: 0.25s; } .plusitemitalic { text-align: center; width: 100%; font-family: "Museo-300"; font-size: 13px; color: #a2c1c6; font-style: italic; position: absolute; top: 0; } .arrowup, .arrowdown { position: absolute; background-size: 538px 84px; width: 21px; height: 12px; } .arrowup { background-position: -30px 0; } .arrowdown { background-position: 0 0; } #incomeup, #incomedown { font-family: "Museo-300"; font-size: 12px; color: #a2c1c6; font-style: italic; position: absolute; bottom: 0px; margin: auto; width: 90px; height: 12px; cursor: pointer; text-align: center; } #incomeup { left: 63%; } #incomedown { left: 35%; } #incomeslider { -webkit-transition: 0.25s; -moz-transition: 0.25s; -o-transition: 0.25s; -ms-transition: 0.25s; transition: 0.25s; position: absolute; /*RELATIVE!*/ bottom: 0px; margin: auto; width: 100%; } /* =============== EXPENSE SLIDER =============== */ .plusitem { top: -8px; left: 9%; margin: auto; position: absolute; cursor: pointer; width: 33px; height: 33px; -webkit-border-radius: 75px; -moz-border-radius: 75px; border-radius: 75px; -webkit-transition: 150ms; -moz-transition: 150ms; -o-transition: 150ms; -ms-transition: 150ms; transition: 150ms; } .plusitemborder { position: absolute; border: 1px solid #aad6df; -webkit-border-radius: 75px; -moz-border-radius: 75px; border-radius: 75px; top:0; left: 0; right: 0; bottom: 0; margin: auto; } .plusitem:hover { -webkit-transform: scale(1.15); -moz-transform: scale(1.15); -o-transform: scale(1.15); -ms-transform: scale(1.15); transform: scale(1.15); } .plusitem:active { -webkit-transform: scale(1); -moz-transform: scale(1); -o-transform: scale(1); -ms-transform: scale(1); transform: scale(1); } .plusitemone, .plusitemtwo { position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: auto; background-color: #aad6df; } .plusitemone { width: 50%; height: 1px; } .plusitemtwo { width: 1px; height: 50%; } #expenseup, #expensedown { font-family: "Museo-300"; font-size: 12px; color: #a2c1c6; font-style: italic; position: absolute; bottom:0px; margin: auto; width: 90px; height: 12px; cursor: pointer; text-align: center; } #expenseup { left: 63%; } #expensedown { left: 35%; } .frame { text-align: left; overflow: hidden; position: absolute; top:50px; left: 0; right: 0; bottom: 0; width: 100%; height: 284px; } .wrapper { position: absolute; top:0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; } #expenseslider { -webkit-transition: 0.25s; -moz-transition: 0.25s; -o-transition: 0.25s; -ms-transition: 0.25s; transition: 0.25s; display: none; position: absolute; /*RELATIVE!*/ bottom: 0px; margin: auto; width: 100%; } .itembackground { display: block; position: relative; top: -26px; left: 25px; } .incomeitem, .expenseitem { -webkit-transition: 350ms ease-out; -moz-transition: 350ms ease-out; -o-transition: 350ms ease-out; -ms-transition: 350ms ease-out; transition: 350ms ease-out; background-position: 25px center; background-repeat: no-repeat; background-size: 42px 42px; border-bottom: 1px solid #e2e2e2; width: 100%; height: 70px; z-index: 500; cursor: pointer; overflow: hidden; } .incomeitem:hover, .expenseitem:hover { background-color: #fbfbfb; } .incomeitem:active, .expenseitem:active { background-color: #f7f7f7; } .newitemadded { -webkit-animation: 4s newitemadded ease-out; -moz-animation: 4s newitemadded ease-out; -o-animation: 4s newitemadded ease-out; -ms-animation: 4s newitemadded ease-out; animation: 4s newitemadded ease-out; } .title11museo300 { font-family: "Museo-100"; font-size: 14px; color: #676767; position: relative; left: 35%; top:12px; } .title9museo300 { font-family: "Museo-300"; font-size: 9px; color: #676767; position: relative; left: 35%; top:20px; } .bolddigit20 { font-family: "Museo-500"; font-size: 20px; color: #111; } .lightdigit20 { font-family: "Museo-100"; font-size: 20px; color: #111; } .lighttitle20 { height: 40px; font-family: "Museo-100"; font-size: 20px; color: #4b4b4b; position: absolute; left: 35%; cursor: pointer; } /* ITEMS AND FRAMES ANIMATION */ .sliderup { -webkit-transform: translateY(71px); -moz-transform: translateY(71px); -o-transform: translateY(71px); -ms-transform: translateY(71px); transform: translateY(71px); } .sliderdown { -webkit-transform: translateY(-71px); -moz-transform: translateY(-71px); -o-transform: translateY(-71px); -ms-transform: translateY(-71px); transform: translateY(-71px); } .endoflist { -webkit-animation: 0.5s endoflist ease-out; -moz-animation: 0.5s endoflist ease-out; -o-animation: 0.5s endoflist ease-out; -ms-animation: 0.5s endoflist ease-out; animation: 0.5s endoflist ease-out; } .startoflist { -webkit-animation: 0.5s startoflist ease-out; -moz-animation: 0.5s startoflist ease-out; -o-animation: 0.5s startoflist ease-out; -ms-animation: 0.5s startoflist ease-out; animation: 0.5s startoflist ease-out; } #expenseslider, #incomeslider { -webkit-animation: 1s frameanimate ease-out; -moz-animation: 0.8s frameanimate ease-out; -o-animation: 0.8s frameanimate ease-out; -ms-animation: 0.8s frameanimate ease-out; animation: 0.8s frameanimate ease-out; } /* NOTES WINDOW */ #add-notes { visibility: hidden; position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: auto; width: 465px; height: 530px; z-index: 2000; opacity: 1; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden; -webkit-perspective: 1300; -o-perspective: 1300px; -ms-perspective: 1300px; -moz-perspective: 1300px; perspective: 1300; } #add-notes >.modal-content { height: 610px; } .notes-title { font-weight: 900; font-family: "Museo-500"; font-size: 18px; color: #242424; position: absolute; width: 240px; height: 20px; margin: auto; right: 0; left: 0; top: 20px; } .notes-save { display: block; position: absolute; width: 100%; height: 42px; bottom: 20px; background-color: #cce5ec; cursor: pointer; line-height: 30px; } .notes-save:hover, .modal-save:hover { background-color: #d6ebf1; } .notes-save:active, .modal-save:active { background-color: #e0edf0; } .notes-input { line-height: 20px; display: none; resize: none; vertical-align: top; border: 1px solid #ccc; background-color: white; position: absolute; top: 78px; left: 0; right: 0; margin: auto; font-family: "Museo-300"; font-size: 13px; padding: 20px; padding-top: 30px; width: 360px; height: 60% } /* MODAL WINDOWS */ #overlay { display: none; visibility: hidden; position: fixed; width: 100%; height: 100%; top: 0; left: 0; z-index: 1000; opacity: 0; background: rgba(0,0,0,0.65); -webkit-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; -moz-transition: all 0.3s; transition: all 0.3s; } #add-modal { visibility: hidden; position: absolute; top:50px; left: 0; right: 0; bottom: 0; margin: auto; width: 465px; height: 530px; z-index: 2000; opacity: 1; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden; -webkit-perspective: 1300px; -o-perspective: 1300px; -ms-perspective: 1300px; -moz-perspective: 1300px; perspective: 1300px; } .modal-content { display: none; visibility: hidden; text-align: center; text-transform: uppercase; background: white; box-shadow: 0 0 15px rgba(0,0,0,0.2); position: relative; height: 480px; -webkit-transform-style: preserve-3d; -moz-transform-style: preserve-3d; transform-style: preserve-3d; -webkit-transform: rotateY(-60deg); -moz-transform: rotateY(-60deg); -ms-transform: rotateY(-60deg); transform: rotateY(-60deg); -webkit-transition: all 0.3s; -o-transition: all 0.3s; -moz-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s; opacity: 0; } .modal-show { opacity: 1; visibility: visible; } .modal-show .modal-content { visibility: visible; -webkit-transform: rotateY(0deg); -moz-transform: rotateY(0deg); -ms-transform: rotateY(0deg); transform: rotateY(0deg); opacity: 1; } #overlay.modal-show { opacity: 1; visibility: visible; } .close-sign { background-position: 0 -22px; background-size: 538px 84px; position: absolute; width: 16px; height: 16px; top: 20px; right: 26px; } .modal-close { position: absolute; width: 86px; height: 62px; top: 0; right: 0; cursor: pointer; z-index: 3500; } .modal-title { font-weight: 900; font-family: "Museo-500"; font-size: 18px; color: #242424; position: absolute; width: 240px; height: 20px; margin: auto; right: 0; left: 0; top: 20px; } .modal-save { display: block; position: absolute; width: 100%; height: 42px; top: 415px; background-color: #cce5ec; cursor: pointer; line-height: 30px; } .modal-save-icon { background-position: -60px 0; background-size: 538px 84px; position: absolute; width: 30px; height: 30px; left: 32%; top: 6px; } .modal-save-text { position: absolute; display: inline-block; margin: auto; top:0; bottom:0; left: 32%; width: 210px; height: 30px; font-family: "Museo-500"; vertical-align: middle; font-size: 18px; color: white; } .modal-delete { -webkit-transition: 0.2s ease-out; -moz-transition: 0.2s ease-out; -ms-transition: 0.2s ease-out; -o-transition: 0.2s ease-out; display: none; position: absolute; width: 56px; height: 42px; top: 415px; right: 0; background-color: #d9ebf0; border-left: 2px solid white; cursor: pointer; line-height: 30px; overflow: hidden; } .modal-delete:hover { width: 380px; background-color: #facece; } .modal-delete:active { background-color: #ffe3e3; } #modaldeletecross { position: absolute; width: 30px; height: 30px; background-position: -320px 0; margin-top: 6px; margin-left: 13px; } .modal-delete-text { background-position: -320px 0; position: absolute; display: inline-block; margin: auto; top:0; bottom:0; left: 13px; width: 190px; height: 30px; font-family: "Museo-500"; vertical-align: middle; font-size: 18px; color: white; } .modalvalue { text-align: center; display: none; border: 0px solid; background-color: transparent; position: absolute; top: 78px; left: 0; right: 0; margin: auto; font-family: "Museo-500"; font-size: 86px; padding: 0; width: 370px; -webkit-transition: 0.3s; -moz-transition: 0.3s; -ms-transition: 0.3s; -o-transition: 0.3s; } .modalvalueerror { color: #ffdd33; -webkit-animation: 0.5s modalvalueerror ease-out; -moz-animation: 0.5s modalvalueerror ease-out; -o-animation: 0.5s modalvalueerror ease-out; -ms-animation: 0.5s modalvalueerror ease-out; animation: 0.5s modalvalueerror ease-out; } .modaltitle { display: none; text-transform: uppercase; position: absolute; top: 345px; left: 0; right: 0; margin: auto; text-align: center; border: 1px solid #ccc; color: #4c4c4c; font-family: "Museo-300", Arial; font-size: 14px; height: 30px; width: 296px; letter-spacing: 1px; -webkit-transition: all 0.4s; -o-transition: all 0.4s; -ms-transition: all 0.4s; -moz-transition: all 0.4s; transition: all 0.4s; } .modaltitleerror { background-color: #ffdd33; } .modalselects { z-index: 2000; text-transform: lowercase; text-align: left; display: block; position: absolute; top: 200px; left: 0; right: 0; margin: auto; width: 260px; height: 30px; } * { margin: 0; padding: 0; } .modalselects .selectbox { color: #333; vertical-align: middle; cursor: pointer; margin: 5px; } .modalselects .selectbox .select { width: 60px; height: 24px; padding: 0 45px 0 10px; font: 12px/24px Arial; border: 1px solid #ccc; } .modalselects .selectbox .select .text { display: block; width: 100%; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .modalselects .selectbox .trigger .arrow { position: absolute; top: 11px; right: 10px; border-left: 3px solid transparent; border-right: 3px solid transparent; border-top: 3px solid #a1a1a1; width: 0; height: 0; overflow: hidden; } .modalselects .selectbox .dropdown { top: 25px; width: 115px; margin: 0; padding: 0 0; background: #FFF; border: 1px solid #ccc; font: 12px Arial; } .initicons-arrow { background-position: -32px -24px; background-size: 538px 84px; position: absolute; width: 15px; height: 9px; right: 0; top: 18px; } .initicons { cursor: pointer; width: 75px; height: 42px; position: absolute; text-align: left; top: 270px; left: 0; right: 0; margin: auto; } .initicons img { width: 42px; height: 42px; } .initicons:hover { opacity: 0.8; } .initicons:active { -webkit-transform: scale(0.92); -moz-transform: scale(0.92); -o-transform: scale(0.92); -ms-transform: scale(0.92); transform: scale(0.92); } .modalincomessurface { position: absolute; z-index: 3000; position: absolute; width: 100%; height: 100%; background-color: white; display: none; } .modalexpensessurface { z-index: 3000; position: absolute; width: 100%; height: 100%; background-color: white; display: none; } #modalexpensetable { text-align: center; background-color: white; position: absolute; top: 14%; left: 42px; } #modalincomestable { text-align: center; background-color: white; position: absolute; top: 30%; left: 42px; } .modaltable tr, .modaltable-low tr { background-color: #ddeef1; } .modaltable td, .modaltable-low td { cursor: pointer; width: 86px; height: 86px; } .imgbox { background-color: white; border: 2px solid white; -webkit-transition: all 0.1s; -o-transition: all 0.1s; -ms-transition: all 0.1s; -moz-transition: all 0.1s; transition: all 0.1s; width: 86px; height: 86px; } .imgbox:hover { border: 2px solid #ddeef1; } .modalborderwhite { border: 2px solid white; } .iconbox { background-color: white; position: absolute; margin:22px; } .auto {background-position: -989px 0;} .gas {background-position: -602px 0;} .home {background-position: -473px 0;} .baby {background-position: -946px 0;} .cart {background-position: -903px 0;} .clothes {background-position: -817px 0;} .phone {background-position: -258px 0;} .utilities {background-position: -43px 0;} .island {background-position: -430px 0;} .earth {background-position: -731px 0;} .meal {background-position: -387px 0;} .sport {background-position: -129px 0;} .medical {background-position: -344px 0;} .tv {background-position: -86px 0;} .smoking {background-position: -172px 0;} .other {background-position: -301px 0;} .edu {background-position: -688px 0;} .graphs {background-position: -516px 0;} .wallet {background-position: 0 0;} .case {background-position: -860px 0;} .rub {background-position: -215px 0;} .euro {background-position: -645px 0;} .doll {background-position: -774px 0;} .gbp {background-position: -559px 0;} #chooseicon { position: absolute; left: 0; top: 0; } .imgbox:active { -webkit-transform: scale(0.8); -moz-transform: scale(0.8); -o-transform: scale(0.8); -ms-transform: scale(0.8); transform: scale(0.8); } .modalforward { -webkit-animation: 0.4s modalforward ease-out; -moz-animation: 0.4s modalforward ease-out; -o-animation: 0.4s modalforward ease-out; -ms-animation: 0.4s modalforward ease-out; animation: 0.4s modalforward ease-out; } .modalreverse { -webkit-animation: 0.3s modalreverse ease-out; -moz-animation: 0.5s modalreverse ease-out; -o-animation: 0.5s modalreverse ease-out; -ms-animation: 0.5s modalreverse ease-out; animation: 0.3s modalreverse ease-out; } /* ============= STATISTIC PAGE STYLES =============*/ /* MAIN BLOCK */ #mainblock { -moz-transition: 0.3s ease-out; -o-transition: 0.3s ease-out; -ms-transition: 0.3s ease-out; -webkit-transition: 0.3s ease-out; transition: 0.3s ease-out; width: 1370px; height: 680px; position: absolute; margin: auto; left:0; right:0; top:22%; } .flag { height: 19px; border: 1px solid #c7dade; font-family: "Museo-100"; font-size: 11px; color: #242424; text-transform: uppercase; font-style: italic; line-height: 20px; padding-left: 20px; left: 0; } #deltatitle { position: absolute; top: -10px; width: 130px; } #savingstitle { position: absolute; bottom: 240px; width: 90px; } .triangle { position: absolute; bottom: -14px; left: -1px; width: 15px; height: 13px; background-position: -523px 0; } /* SAVINGS CHART */ #savingschart { position: absolute; width: 504px; height: 222px; bottom: 50px; right: 0; } #horizontal, #chartline { position: absolute; } #month0 { position: absolute; left:0; bottom: 0; display: block; margin-right: -4px; width: 0; height: 68px; border-left: 1px solid #cdc6c6; } #month0 .small-month-circle, #month0 .month-name { display: block; } #month0 .month-name { bottom: 32%; } #month12 .month-name { width: 20px; height: 16px; } .months { position: relative; display: inline-block; margin-right: -4px; width: 42px; height: 100%; } .in-month { position: absolute; bottom: 0; width: 100%; height: 50%; } .small-month-circle { position: absolute; right: -5px; top: -4px; width: 7px; height: 7px; border: 1px solid #b9b2b4; border-radius: 20px; background-color: white; } .large-month-circle { position: absolute; right: -10px; top: -10px; width: 17px; height: 17px; border: 1px solid #b9b2b4; border-radius: 20px; background-color: white; display: block; } .bluedot { position: absolute; right: 3px; top: 3px; width: 11px; height: 11px; border-radius: 20px; background-color: #d5e4e7; } .large-month-val { text-transform: uppercase; text-align: right; position: absolute; top: -34px; right: 5px; width: 160px; height: 20px; font-family: "Museo-500"; font-size: 22px; color: #333; } .large-month-val .curr { font-family: "Museo-100"; font-size: 12px; } .month-val { text-transform: uppercase; text-align: center; position: absolute; bottom: -32px; left: -18px; width: 120px; height: 14px; font-family: "Museo-300"; font-size: 15px; color: #333; display: none; background-color: white; } .month-val .curr { font-family: "Museo-100"; font-size: 10px; } .month-name { position: absolute; bottom: 40%; right: -11px; font-family: "Museo-300"; text-transform: uppercase; text-align: center; font-size: 10px; color: #948b8e; background-color: white; height: 21px; width: 25px; line-height: 21px; display: none; } #month6, #month12{ border-right: 1px solid #cdc6c6; } #month6 .small-month-circle, #month6 .month-name, #month12 .month-name { display: block; } .months:hover .in-month { border-right: 1px solid #cdc6c6; } .months:hover .small-month-circle, .months:hover .month-name, .months:hover .month-val { display: block; } /* SAVINGS SLIDER */ #savings-slider-container { width: 260px; height: 50px; position: absolute; bottom: 70px; left: 40px; } .savings-slider { text-transform: uppercase; position: absolute; top: -90px; font-family: "Museo-300"; font-size: 11px; color: #5e5e5e; } #savingsTip0, #savingsTip100 { position: absolute; width: 100%; height: 35px; font-family: "Museo-300"; font-style: italic; font-size: 11px; color: #a59b9e; display: none; } #savingsTip0 { bottom: -5px; left: 38px; } #savingsTip100 { bottom: -5px; } .noUi-target,.noUi-target * { -webkit-touch-callout:none; -webkit-user-select:none; -ms-touch-action:none; -ms-user-select:none; -moz-user-select:none; -moz-box-sizing:border-box; box-sizing:border-box } .noUi-base { width:100%; height:100%; position:absolute; } .noUi-origin { position:absolute; right:0; top:0; } .noUi-handle { content: ""; position:relative; z-index:1; border: 1px solid #c4d5d9; cursor:default; -webkit-border-radius: 23px; -ms-border-radius: 23px; -o-border-radius: 23px; -moz-border-radius: 23px; border-radius: 23px; background-position: -516px -17px; } .noUi-handle-upper { width: 3px; } .noUi-stacking .noUi-handle { z-index:10 } .noUi-stacking+.noUi-origin { z-index:-1 } .noUi-state-tap .noUi-origin { -webkit-transition:left .3s,top .3s; transition:left .3s,top .3s } .noUi-state-drag * { cursor:inherit!important } .noUi-horizontal { height: 4px } .noUi-horizontal .noUi-handle { width:23px; height:23px; left:-12px; top:-9px } .noUi-background { background:#d9e7ea; border-left: 2px solid #a39a9d; border-right: 2px solid #a39a9d; } .noUi-dragable { cursor: w-resize } .noUi-vertical .noUi-dragable { cursor:n-resize; } .noUi-active { border: 1px solid #95b4b8; } .noUi-handle:after { content: attr(percent); text-align: center; display:block; position:absolute; width: 30px; height: 30px; left: 4px; top: -38px; font-family: "Museo-300"; font-size: 11px; color: #2f2f2f; background-position: -414px 0; line-height: 18px; } .noUi-handle:before { content: attr(value); text-align: center; display:block; position:absolute; height:1px; width:180px; height: 15px; left: -80px; top: 35px; font-family: "Museo-100"; font-size: 16px; color: #2f2f2f; } [disabled] .noUi-connect,[disabled].noUi-connect { background:#B8B8B8 } [disabled] .noUi-handle { cursor:not-allowed } /* SAVINGS CIRCLE */ #savingscircles { text-align: center; width: 385px; height: 385px; position: absolute; left: 390px; bottom: 30px; z-index: 800; } #after-savings, #before-savings { border: 1px solid #c9dadd; -webkit-border-radius: 350px; -ms-border-radius: 350px; -o-border-radius: 350px; -moz-border-radius: 350px; border-radius: 350px; } #after-savings { width: 82%; height: 82%; position: absolute; right:0; top:0; } #before-savings { width: 46%; height: 46%; position: absolute; left:0; bottom:0; } .savings-circle-title { font-size: 12px; position: absolute; margin: auto; top: 30%; left:0; right:0; } .savings-circle-currency { font-size: 11px; position: absolute; margin: auto; bottom: 22%; left:0; right:0; } #before-savings-value { position: absolute; width: 130%; height: 100%; margin: auto; left:-15%; right: 0; text-align: center; line-height: 165px; font-size: 30px; top: 8%; } #after-savings-value { width: 130%; height: 100%; position: absolute; margin: auto; left:-15%; right: 0; top:7%; text-align: center; line-height: 290px; } /* LINES CONTAINER */ #incomes-lines-container, #expenses-lines-container { -moz-transition: 0.3s ease-out; -o-transition: 0.3s ease-out; -ms-transition: 0.3s ease-out; -webkit-transition: 0.3s ease-out; transition: 0.3s ease-out; background-color: white; width: 270px; height: 300px; position: absolute; left: 375px; top: 0; z-index: 200; padding-top: 65px; padding-left: 30px; } #incomes-lines-container { display: none; } .lines-title { cursor: pointer; position: absolute; width: 400px; top: 35px; text-transform: uppercase; font-family: "Museo-300"; font-size: 13px; color: black; } .lineitemtitle { position: absolute; top: 15px; width: 280px; height: 70px; } .lineitemvalue { font-family: "Museo-300"; font-size: 25px; position: absolute; top: 15px; left: 64px; top:50px; width: 180px; } .lineitemcurr { font-size: 11px; } .lineitempercent { font-family: "Museo-500"; font-size: 10px; color: #928588; position: absolute; top: -8px; left: -27px; } .grey { color: #adadad; } .greysmall { font-size: 9px; } .itemline { -moz-transition: 0.5s ease-out; -o-transition: 0.5s ease-out; -ms-transition: 0.5s ease-out; -webkit-transition: 0.5s ease-out; transition: 0.5s ease-out; font-size: 13px; cursor: pointer; position: relative; height: 9px; border-top: 2px solid #d5e4e7; overflow: hidden; background-color: white; width: 1%; } .itemlinebackground { position: absolute; top: 36px; left:0; width: 42px; height: 42px; } .leftpoint { width: 2px; height: 2px; background-color: #a39a9d; position: absolute; top:-2px; left:0; } .activeline { border-top: 4px solid #a39a9d; height: 88px; overflow: visible; } /* LINES CURSOR */ #expense-cursor { -webkit-transform: rotate(68deg); -moz-transform: rotate(68deg); -ms-transform: rotate(68deg); -o-transform: rotate(68deg); transform: rotate(68deg); width: 310px; height: 310px; position: absolute; left: 225px; top: -106px; z-index: 100; display: none; cursor: pointer; } #incomes-cursor { -webkit-transform: rotate(52deg); -moz-transform: rotate(52deg); -ms-transform: rotate(52deg); -o-transform: rotate(52deg); transform: rotate(52deg); width: 370px; height: 370px; position: absolute; left: 227px; top: -120px; z-index: 100; display: none; cursor: pointer; } .cursorline { width: 1px; height: 50%; position: absolute; bottom: 0; left: 149px; background-color: #727272; } .cursorpoint { position: absolute; bottom: -3px; left: 147px; width: 6px; height: 6px; border-radius: 10px; background-color: #727272; } /* LARGE CIRCLE */ #outermaindiv { color: black; position: absolute; width: 265px; height: 265px; position: absolute; top: 50px; left:10px; z-index: 0; cursor: pointer; } #innermaindiv { position: absolute; width: 100%; height: 100%; top:0; left:0; margin: 21px; } #outermaincursordiv { position: absolute; width: 100%; height: 100%; top:0; z-index: 500; margin: -19px; } .linesbackground { position: absolute; width: 100%; height: 100%; background-image: url("../images/linesbackground.png"); background-repeat: repeat; } .topmaincircletitle { width: 100px; height: 30px; text-align: center; position: absolute; margin: auto; left: 0; right: 0; top:84px; font-size: 15px; font-style: italic; } .bottommaincircletitle { width: 100px; height: 30px; text-align: center; position: absolute; margin: auto; left: 0; right: 0; bottom:60px; font-size: 13px; } #outer-circle-value { width: 100%; height: 100%; text-align: center; position: absolute; margin: auto; line-height: 265px; top: 5px; font-size: 48px; } .lightcircletitle { font-family: "Museo-100"; text-transform: uppercase; } .boldcircletitle { font-family: "Museo-500"; text-transform: uppercase; } #maincircle100percent { width: 30px; height: 15px; position: absolute; top: 50px; right: 100px; font-size: 12px; } #maincircleline { width: 1px; height: 27px; position: absolute; top: 17px; right: 131px; background-color: #898989; box-shadow: -1px 0 1px #acacac; } #outer-circle-percent { display: none; width: 30px; height: 30px; position: absolute; left: 0; right: 0; top: 0; font-size: 11px; z-index: 1000; } /* PENDANTS */ #firstpendant { overflow: visible; z-index: -100; position: absolute; bottom: 0; left:60px; width: 60px; height: 100px; } #firstpendant > .pendant-circle { bottom: 77px; border: 2px solid #d3d8ce; } #firstpendant > .pendantline { bottom: 90px; background-color: #d3d8ce; } #firstpendant > .pendantfont { bottom: 50px; } #secondpendant { overflow: visible; z-index: -100; position: absolute; bottom: 0; left:282px; width: 45px; height: 100px; } #secondpendant > .pendant-circle { bottom: 57px; border: 2px solid #b9b2b4; } #secondpendant > .pendantline { bottom: 70px; background-color: #b9b2b4; } #secondpendant > .pendantfont { bottom: 30px; } #thirdpendant { overflow: visible; z-index: -100; position: absolute; bottom: 0; right:60px; width: 45px; height: 100px; } #thirdpendant > .pendant-circle { bottom: 37px; border: 2px solid #abb1bf; } #thirdpendant > .pendantline { bottom: 50px; background-color: #abb1bf; } #thirdpendant > .pendantfont { bottom: 10px; } .pendant-circle { position: absolute; left: 17px; width: 8px; height: 8px; border-radius: 20px; } .pendantfont { position: absolute; text-align: center; width: 70px; font-size: 11px; font-style: italic; left: -12px; } .pendantline { position: absolute; left: 22px; width: 1px; height: 120%; } /* SMALL CIRCLES */ #movingcircle-1, #movingcircle-2, #movingcircle-3 { -webkit-animation: 1s spincircle ease-out infinite; -moz-animation: 1s spincircle ease-out infinite; -o-animation: 1s spincircle ease-out infinite; -ms-animation: 1s spincircle ease-out infinite; animation: 1s spincircle ease-out infinite; } .flippedcard { -webkit-transform: rotateY(-180deg); -moz-transform: rotateY(-180deg); -ms-transform: rotateY(-180deg); -o-transform: rotateY(-180deg); transform: rotateY(-180deg); } .flippedcardinfo { -webkit-transform: rotateY(180deg); -moz-transform: rotateY(180deg); -ms-transform: rotateY(180deg); -o-transform: rotateY(180deg); transform: rotateY(180deg); } #small-circles-container { position: absolute; right: 0; top: 40px; width: 608px; height: 290px; overflow: visible; } #firstcirclediv, #secondcirclediv, #thirdcirclediv { -webkit-perspective: 1000; -moz-perspective: 1000px; -ms-perspective: 1000px; -o-perspective: 1000px; perspective: 1000; -moz-transition: 0.3s ease-out; -o-transition: 0.3s ease-out; -ms-transition: 0.3s ease-out; -webkit-transition: 0.3s ease-out; transition: 0.3s ease-out; cursor: pointer; position: relative; top: 0; width: 200px; height: 180px; display: inline-block; background-color: white; } /* Backfaces */ .circlesselect { z-index: 2000; text-transform: uppercase; text-align: center; display: block; position: absolute; top: 94px; left: 0; right: 20px; margin: auto; width: 136px; height: 80px; } .circlesselect > .selectbox .select { position: absolute; margin: auto; width: 136px; height: 22px; padding: 0 10px 0 10px; font: 10px/23px "Museo-500"; border: 1px solid #ccc; } .circlesselect > .selectbox .dropdown { top: 23px; width: 156px; max-height: 220px; margin: 0; padding: 0 0; background: #FFF; border: 1px solid #ccc; font-family: "Museo-300"; font-size: 10px; } #circle-select-1-back, #circle-select-2-back, #circle-select-3-back { position: absolute; margin: auto; left: 0; right: 0; top: 30px; width: 42px; height: 42px; } .flippedcircle { -webkit-transform: rotateY(-180deg); -moz-transform: rotateY(-180deg); -ms-transform: rotateY(-180deg); -o-transform: rotateY(-180deg); transform: rotateY(-180deg); } .frontcircle, .backcircle { -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -ms-backface-visibility: hidden; -o-backface-visibility: hidden; backface-visibility: hidden; overflow: visible; position: absolute; } .frontcircle { -moz-transition: 0.3s ease-out; -o-transition: 0.3s ease-out; -ms-transition: 0.3s ease-out; -webkit-transition: 0.3s ease-out; transition: 0.3s ease-out; position: absolute; margin: auto; right: 0; left: 0; z-index: 2; width: 165px; height: 165px; } .backcircle { -webkit-transform: rotateY(180deg); -moz-transform: rotateY(180deg); -ms-transform: rotateY(180deg); -o-transform: rotateY(180deg); transform: rotateY(180deg); z-index: 1; position: absolute; top: -30px; left: 0; right: 0; width: 200px; height: 200px; } .backcircle .circletoptitle { width: 170px; height: 50px; line-height: 14px; bottom: -85px; } #firstcirclediv { left: -16px; } #secondcirclediv { left: 0; } #thirdcirclediv { left: 16px; } #firstcircledivflipper, #secondcircledivflipper, #thirdcircledivflipper { -webkit-transform-style: preserve-3d; -moz-transform-style: preserve-3d; -ms-transform-style: preserve-3d; -o-transform-style: preserve-3d; transform-style: preserve-3d; -webkit-transition: 1000ms; -moz-transition: 1000ms; -ms-transition: 1000ms; -o-transition: 1000ms; transition: 1000ms; position: relative; overflow: visible; } .frontcircle:active { -webkit-transform: scale(0.8); -moz-transform: scale(0.8); -o-transform: scale(0.8); -ms-transform: scale(0.8); transform: scale(0.8); } .cursordiv { position: absolute; width: 100%; height: 100%; top: 0; z-index: 600; margin: -12px; } .circletoptitle { -moz-transition: 0.3s ease-out; -o-transition: 0.3s ease-out; -ms-transition: 0.3s ease-out; -webkit-transition: 0.3s ease-out; transition: 0.3s ease-out; text-align: center; overflow: hidden; font-style: italic; font-size: 10px; position: absolute; width: 120px; height: 12px; margin: auto; left: 0; right: 0; top: 52px; } .circlevalue { position: absolute; width: 100%; height: 100%; top: 4px; text-align: center; line-height: 165px; font-size: 31px; } .bottomcircletitle { width: 100px; height: 30px; text-align: center; position: absolute; margin: auto; left: 0; right: 0; top: 110px; font-size: 10px; } #first-circle-percent, #second-circle-percent, #third-circle-percent { display: none; width: 30px; height: 30px; position: absolute; left: 0; right: 0; top: 0; font-size: 11px; } .circle100percent { width: 30px; height: 15px; position: absolute; top: 30px; right: 52px; font-size: 9px; } .circleline { width: 1px; height: 17px; position: absolute; top: 11px; right: 82px; background-color: #898989; } #setcurrency { right: -18px; top: -15%; position: absolute; width: 170px; height: 42px; } #rubcurr, #eurcurr, #usdcurr { vertical-align: top; text-align: center; position: relative; font-size: 11px; text-transform: uppercase; color: #444; display: inline-block; width: 42px; height: 42px; border-radius: 42px; margin-right: 7px; line-height: 43px; } #rubcurr:hover, #eurcurr:hover, #usdcurr:hover { border: 2px solid #f6f6f6; } .currunchecked { cursor: pointer; font-family: "Museo-100"; border: 2px solid white; } .currchecked { cursor: default; font-family: "Museo-500"; border: 2px solid #e8e8e8; } /* ============== CONDITIONS FOR DIFFERENT SCREEN SIZES ============== */ @media screen and (max-height: 870px) { #mainblock { height: 620px; top:19%; } #savingschart { bottom: 10px; } #savingscircles { bottom: 0px; } #savings-slider-container { bottom: 20px; } #savingstitle { bottom: 190px; } #small-circles-container { height: 274px; } } @media screen and (max-height: 780px) { #mainblock { height: 590px; top:17%; } #savingschart { bottom: 20px; } #savingscircles { bottom: 25px; } #savings-slider-container { bottom: 30px; } #savingstitle { bottom: 200px; } #logo_statistic { top: -25px; -webkit-transform: rotateY(180deg) scale(0.8); -moz-transform: rotateY(180deg) scale(0.8); -ms-transform: rotateY(180deg) scale(0.8); -o-transform: rotateY(180deg) scale(0.8); transform: rotateY(180deg) scale(0.8); } #small-circles-container { height: 274px; } #setcurrency { top: -12%; } #infopage { height: 610px; margin-top: -70px; } #infosubtitle { top: 40px; } #infotitle { top: 75px; } } @media screen and (max-height: 700px) { .month-val { bottom: 5px; } #mainblock { height: 582px; top:14%; } #savingschart { bottom: 20px; } #savingscircles { bottom: 10px; } #savings-slider-container { bottom: 30px; } #savingstitle { bottom: 200px; } #logo_statistic { top: -35px; -webkit-transform: rotateY(180deg) scale(0.7); -moz-transform: rotateY(180deg) scale(0.7); -ms-transform: rotateY(180deg) scale(0.7); -o-transform: rotateY(180deg) scale(0.7); transform: rotateY(180deg) scale(0.7); } #small-circles-container { height: 270px; } #setcurrency { top: -10%; } } @media screen and (max-width: 1100px) { .columns { width: 300px; top:24%; } #savings { left: 20%; right: -450px; bottom: 0; } #incomes { left: -20%; right: 450px; bottom: 0; } #expenses { left: 0; right: 0; bottom: 0; } } @media screen and (min-width: 1100px) { .columns { width: 340px; top:24%; } #savings { left: 21%; right: -490px; bottom: 0; } #incomes { left: -21%; right: 490px; bottom: 0; } #expenses { left: 0; right: 0; bottom: 0; } } @media screen and (max-height: 850px) { #logo_greeting, #logo_settings { background: url("../images/logo_large.gif") no-repeat; background-position: top center; background-size: 137px 204px; z-index: 1000; width: 172px; height: 204px; top: 5%; } #settings_hat { height: 28%; } .columns { top:16%; } #logo_settings { height: 130px; top: 8%; } #savebutton { bottom: 3%; } #incomeup, #incomedown, #expenseup, #expensedown { bottom: 20px; } } @media screen and (max-height: 700px) { #settings_hat { height: 22%; } #add-notes >.modal-content { height: 530px; } .notes-input { height: 56% } #logo_greeting, #logo_settings { background: url("../images/logo.gif") no-repeat; background-size: 114px 145px; width: 114px; height: 145px; top: 8%; } .columns { top:16%; } #logo_settings { height: 100px; } #bubble { right: -70px; } #savebutton { bottom: 1%; } #incomeup, #incomedown, #expenseup, #expensedown { bottom: 20px; } #piggy { background: url("../images/piggy.gif") no-repeat; background-size: 149px 149px; width: 149px; height: 149px; } #logotext { background: url("../images/logotext.gif") no-repeat; size: 163px 67px; width: 163px; height: 67px; margin-top: 24px; } #wrapper { top: 305px; } #secondenter{ bottom: 115px; } #preloader{ bottom: 99px; } } /* STATISTIC PAGE CONDITIONS */ @media screen and (max-width: 1500px) { #mainblock { width: 1250px; } #incomes-lines-container, #expenses-lines-container { left: 315px; width: 210px; } #savingscircles { left: 330px; } #savingscircles { left: 355px; width: 345px; height: 345px; } #after-savings-value { top: 2%; } #before-savings-value { top: 0; } } @media screen and (max-width: 1200px) { #mainblock { width: 1060px; } #incomes-lines-container, #expenses-lines-container { left: 350px; width: 260px; padding-top: 72px; } #savingschart { width: 396px; height: 222px; } .months { width: 33px; } #savingscircles { left: 345px; width: 300px; height: 300px; } #after-savings-value { top:-5%; } #before-savings-value { top:-7%; } #firstcirclediv, #firstpendant { opacity: 0; } #small-circles-container { height: 260px; } #before-savings-value { font-size: 24px; } } /*================ @2x IMAGES FOR RETINA DISPLAYS ================ */ @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) { #logo_greeting, #logo_settings { background-image: url("../images/logo_large@2x.gif"); } #logo_statistic { background: url("../images/logotext_large@2x.gif"); background-size: 172px 65px; } .imgbox:hover { border: 2px solid white; } } ================================================ FILE: gateway/src/main/resources/static/index.html ================================================ Piggy Metrics
metrics
last seen:
Get in
Log out
Add income Empty space
forth
back
Add expense Empty space
forth
back
My spare money At the moment
Interest
Usd
Eur
Rub
Incomes — expenses
Savings
/ Month
100%
/ Hour
100%
Choose any
item
/ Day
100%
Choose any
item
/ Year
100%
Choose any
item
Per hour
Per day
Per year
part of the spare money which you'll send to accumulation account every month:
Without monthly deposit refill
All savings go to deposit every month
In a year
At the moment
Piggy Metrics is the new simple way to deal with personal finances This is how it works
enter planned periodic
incomes and expenses
Plan or estimate your regular expenses. Divide them into categories. Сomponents of your budget
will always be near to hand
check all your savings
at the moment
Regularly update the value of your savings.
Follow the progress.
analyze financial statistics
and forecasts
Based on this information, you’ll get statistics for your budget and forecast of savings
for the year ahead
Try without registration
© 2016 sqshq.com icons attribution


Thank you for registration.
We suggest you to enter an e-mail address so we can occasionally remind you about the service. Continuous tracking your budget statistics might be especially effective.
================================================ FILE: gateway/src/main/resources/static/js/dashboard.js ================================================ /** * KNOB circles initialization */ $("#inner-circle").knob({ "readOnly": true, "width": 223, "height": 223, "thickness": 0.1, "displayInput": false, "fgColor": "#e6eff1" }); $("#outer-circle").data({ "width": 265 }).knob({ "readOnly": true, "width": 265, "height": 265, "thickness": 0.13, "displayInput": false, "fgColor": "#a59b9e" }); $("#outer-circle-cursor").knob({ "cursor": 0.5, "readOnly": true, "width": 303, "height": 303, "thickness": 0.24, "displayInput": false, "fgColor":"#898989" }); $("#first-circle").data({ "width": 165 }).knob({ "readOnly": true, "width": 165, "height": 165, "thickness": 0.14, "displayInput": false, "fgColor": "#efefef" }); $("#second-circle").data({ "width": 165 }).knob({ "readOnly": true, "width": 165, "height": 165, "thickness": 0.14, "displayInput": false, "fgColor": "#e0ded5" }); $("#third-circle").data({ "width": 165 }).knob({ "readOnly": true, "width": 165, "height": 165, "thickness": 0.14, "displayInput": false, "fgColor": "#b6aeb0" }); $("#first-circle-cursor, #second-circle-cursor, #third-circle-cursor").knob({ "cursor": 0.5, "readOnly": true, "width": 189, "height": 189, "thickness": 0.25, "displayInput": false, "fgColor":"#898989" }); function getConverted(column) { var firstitem, seconditem; for (var key in column) { switch (column[key].currency) { case "RUB": column[key].converted = column[key].amount; break; case "EUR": column[key].converted = (column[key].amount * global.eur).toFixed(3); break; case "USD": column[key].converted = (column[key].amount * global.usd).toFixed(3); break; } switch (column[key].period) { case "MONTH": break; case "HOUR": column[key].converted = (column[key].converted * 730).toFixed(3); break; case "DAY": column[key].converted = (column[key].converted * 30.41666667).toFixed(3); break; case "QUARTER": column[key].converted = (column[key].converted / 3).toFixed(3); break; case "YEAR": column[key].converted = (column[key].converted / 12).toFixed(3); break; } switch (user.checkedCurr) { case "RUB": break; case "EUR": column[key].converted = (column[key].converted / global.eur).toFixed(3); break; case "USD": column[key].converted = (column[key].converted / global.usd).toFixed(3); break; } if (column == incomes) { if (firstitem == undefined) {firstitem = key; $("#incomeslider").data("firstitem", key); } incomesSumMonth += Math.round(column[key].converted); $("#circle-select-1, #circle-select-2, #circle-select-3").append(''); } else { if (firstitem == undefined) {firstitem = key; $("#expenseslider").data("firstitem", key); } else if (seconditem == undefined) {seconditem = key; $("#expenseslider").data("seconditem", key); } expensesSumMonth += Math.round(column[key].converted); $("#circle-select-1, #circle-select-2, #circle-select-3").append(''); } } if ( Math.abs(incomesSumMonth-expensesSumMonth) < 10 ) { expensesSumMonth = incomesSumMonth; } $('select').trigger('refresh'); } function initStatisticPage() { changeCurrency = function () { switch (user.checkedCurr) { case "RUB": if (user.lastCurr == "RUB") { break; } else if (user.lastCurr == "USD") { savings.freeMoney = (savings.freeMoney * global.usd).toFixed(3); } else if (user.lastCurr == "EUR") { savings.freeMoney = (savings.freeMoney * global.eur).toFixed(3); } break; case "EUR": if (user.lastCurr == "EUR") { break; } else if (user.lastCurr == "USD") { savings.freeMoney = (savings.freeMoney * global.usd / global.eur).toFixed(3); } else if (user.lastCurr == "RUB") { savings.freeMoney = (savings.freeMoney / global.eur).toFixed(3); } break; case "USD": if (user.lastCurr == "USD") { break; } else if (user.lastCurr == "EUR") { savings.freeMoney = (savings.freeMoney * global.eur / global.usd).toFixed(3); } else if (user.lastCurr == "RUB") { savings.freeMoney = (savings.freeMoney / global.usd).toFixed(3); } break; } user.lastCurr = user.checkedCurr; }; changeCurrency(); initCircle = function (whichcircle, item, beforevalue) { var sum, value, circle; $("#" + whichcircle + "circlediv").show(); if ( item.hasOwnProperty("income_id") ) { sum = incomesSumMonth; value = item.income_id; } else { sum = expensesSumMonth; value = parseInt(item.expense_id, 10) + 100; } switch (whichcircle) { case "first": animatecircle.call($("#first-circle"), beforevalue, (item.converted / 730).toFixed(2), (sum / 730), item.title ); circle = 1; $("#first-circle").data({"item": item, "sum": sum}); break; case "second": animatecircle.call($("#second-circle"), beforevalue, (item.converted / 30.41666667).toFixed(1), (sum / 30.41666667), item.title ); circle = 2; $("#second-circle").data({"item": item, "sum": sum}); break; case "third": animatecircle.call($("#third-circle"), beforevalue, Math.round(item.converted * 1.2) * 10, (sum * 1.2) * 10, item.title ); circle = 3; $("#third-circle").data({"item": item, "sum": sum}); break; } $("#circle-select-" + circle).find('option').removeAttr("selected"); setTimeout( function() { $("#circle-select-" + circle +" option[value=" + value + "]").attr('selected','selected'); $('select').trigger('refresh'); } , 100) $("#circle-select-" + circle + "-back").removeClass().addClass(item.icon); } if (incomes[ $("#incomeslider").data("firstitem") ] != undefined && expenses[ $("#expenseslider").data("firstitem") ] != undefined) { initCircle("first", incomes[ $("#incomeslider").data("firstitem") ], 0); initCircle("second", expenses[ $("#expenseslider").data("firstitem") ], 0); if ( $("#expenseslider").data("seconditem") !== undefined ) { initCircle("third", expenses[ $("#expenseslider").data("seconditem") ], 0); } else initCircle("third", expenses[ $("#expenseslider").data("firstitem") ], 0); } $("#expenses-lines-container").html('
Expenses structure (' + separateNumber(expensesSumMonth) + '/Month)
') $("#incomes-lines-container").html('
Incomes structure (' + separateNumber(incomesSumMonth) + '/Month)
') var incomesIdSorted = Object.keys(incomes).sort(function(a,b){return incomes[b].converted - incomes[a].converted}); var expensesIdSorted = Object.keys(expenses).sort(function(a,b){return expenses[b].converted - expenses[a].converted}); initlines = function(column) { var container, idSorted, maxConvertedId, i; if (column == incomes) {container = "#incomes-lines-container"; idSorted = incomesIdSorted; maxConvertedId = incomesIdSorted[0]; sum = incomesSumMonth; i=0} else {container = "#expenses-lines-container"; idSorted = expensesIdSorted; maxConvertedId = expensesIdSorted[0]; sum = expensesSumMonth; i=100} idSorted.forEach(function(id) { $(container).append('
' + column[id].title + '
'+ Math.round(100 * column[id].converted / sum) +'%
' + separateNumber(Math.round(column[id].converted)) + ' /Month
'); $("#line-" + i).data({"item": column[id]}).css({"width": Math.round(100 * column[ id ].converted / column[maxConvertedId].converted ) + "%"}); $("#linebackground-" + i).addClass(column[id].icon); i++; }); $("#line-0, #line-100").addClass("activeline"); } initlines(incomes); initlines(expenses); // FUTURE SAVINGS CALCULATION initSavingsCircles = function (slidervalue, lastslidervalue, beforevalue, beforemiddlevalue, animatetime) { var monthSavings = [], depositMonthSavings = [], delta = incomesSumMonth - expensesSumMonth, deltaDeposit = delta * slidervalue, deltaNoDeposit = delta * (1 - slidervalue), percent = savings.percent, deltapercents = 0; monthSavings[0] = parseInt(savings.freeMoney, 10); // Set deposit tips next to slider if (slidervalue === 0) { $("#savingsTip100").hide(); $("#savingsTip0").fadeIn(200); } else if (slidervalue === 1) { $("#savingsTip0").hide(); $("#savingsTip100").fadeIn(200); } else { $("#savingsTip0, #savingsTip100").fadeOut(200); } if (delta >= 0) { // if incomes > expenses if (savings.deposit) { // deposit turned on if (savings.capitalization) { // capitalization turned on (percent adds every month) if (delta === 0) { // delta = 0 $('#savings-slider').css({"opacity": "0.5"}).attr('disabled', 'disabled'); $("#savingsTip100, #savingsTip0").hide(); for (var k=1; k < 13; k++) { monthSavings[k] = ( parseInt(monthSavings[ (k-1) ], 10) + delta ); } if (monthSavings[0] === 0) { // Savings = 0 // Draw horizontal chart $({ value: 91 }).animate({ value: 155 }, { duration: animatetime, easing: 'swing', step: function () { drawChartLine(this.value); } }); } else { // Savings > 0 // Draw increasing chart $({ value: 91 }).animate({ value: 70 }, { duration: animatetime, easing: 'swing', step: function () { drawChartLine(this.value); } }); } } else { // delta > 0 $('#savings-slider').css({"opacity": "1"}).removeAttr('disabled'); // Animate chart $({ value: lastslidervalue}).animate({ value: slidervalue }, { duration: animatetime, easing: 'swing', step: function () { drawChartLine(110 - (this.value) * 95); } }); } depositMonthSavings[0] = monthSavings[0]; for (var i=1; i < 13; i++) { depositMonthSavings[i] = ( deltaDeposit + parseInt(depositMonthSavings[ (i-1) ], 10) + ( parseInt(depositMonthSavings[ (i-1) ], 10) * percent * 30.41666667 / 36500 ) ) // including delta in the last MONTH! monthSavings[i] = (depositMonthSavings[i] + (deltaNoDeposit * i)); } } else { // capitalization turned off (percent adds in the last MONTH) if (delta === 0) { $('#savings-slider').css({"opacity": "0.5"}).attr('disabled', 'disabled'); $("#savingsTip100, #savingsTip0").hide(); if (monthSavings[0] === 0) { // Draw horizontal chart $({ value: 91 }).animate({ value: 155 }, { duration: animatetime, easing: 'swing', step: function () { drawChartLine(this.value); } }); } else { // Draw increasing chart $({ value: 91 }).animate({ value: 80 }, { duration: animatetime, easing: 'swing', step: function () { drawChartLine(this.value); } }); } } else { $('#savings-slider').css({"opacity": "1"}).removeAttr('disabled'); // Animate chart $({ value: lastslidervalue}).animate({ value: slidervalue }, { duration: animatetime, easing: 'swing', step: function () { drawChartLine(110 - (this.value) * 95); } }); } for (var j=1; j < 12; j++) { monthSavings[j] = ( parseInt(monthSavings[ (j-1) ], 10) + deltaDeposit ); deltapercents = deltapercents + ( deltaDeposit * percent * j / 1200 ); } monthSavings[12] = (monthSavings[0] * (1 + (savings.percent / 100)) ) + (deltaDeposit * 12) + (deltaNoDeposit * 12) + deltapercents; // including delta in the last month! } } else { // deposit turned off $('#savings-slider').css({"opacity": "0.5"}).attr('disabled', 'disabled'); $("#savingsTip100, #savingsTip0").hide(); $('.noUi-handle:after').css({"opacity": "0"}) for (var k=1; k < 13; k++) { monthSavings[k] = ( parseInt(monthSavings[ (k-1) ], 10) + delta ); } if (delta === 0) { // Draw horizontal chart $({ value: 91 }).animate({ value: 155 }, { duration: animatetime, easing: 'swing', step: function () { drawChartLine(this.value); } }); } else { // Draw increasing chart $({ value: 91 }).animate({ value: 120 }, { duration: animatetime, easing: 'swing', step: function () { drawChartLine(this.value); } }); } } } else { // if incomes < expenses $('#savings-slider').css({"opacity": "0.5"}).attr('disabled', 'disabled'); $("#savingsTip100, #savingsTip0").hide(); $('.noUi-handle:after').css({"opacity": "0"}); for (var n=1; n < 13; n++) { monthSavings[n] = ( parseInt(monthSavings[ (n-1) ], 10) + delta ); } // Draw decreasing chart $({ value: 91 }).animate({ value: 175 }, { duration: animatetime, easing: 'swing', step: function () { drawChartLine(this.value); } }); } var aftervalue = monthSavings[12]; $("#before-savings-value").html( separateNumber( Math.round(savings.freeMoney) ) ); $("#after-savings-value").data({"lastvalue": aftervalue, "lastmiddlevalue": monthSavings[6]}).html( separateNumber(Math.round((savings.freeMoney * 10.222) / 100) * 100) ); $("#savings-slider").data("lastpercent", $("#savings-slider").data("checkedPercent")); // Set font-size in big circle (function() { if (Math.abs (aftervalue) < 9999999 ) { $("#after-savings-value").css({"font-size": "48px"}) } else if (Math.abs (aftervalue) < 99999999) { $("#after-savings-value").css({"font-size": "42px"}) } else { $("#after-savings-value").css({"font-size": "37px"}) } if (Math.abs (beforevalue) > 9999999 ) { $("#before-savings-value").css({"font-size": "26px"}) } })(); // Animate last value on chart $({ value: beforevalue }).animate({ value: aftervalue }, { duration: animatetime, easing: 'linear', step: function () { $("#after-savings-value").html(separateNumber( Math.round(this.value / 10) * 10)); $("#month12").children(".large-month-val").children(".val").html(separateNumber(Math.round(this.value / 10) * 10)); } }); // Animate middle value on chart $({ value: beforemiddlevalue }).animate({ value: monthSavings[6] }, { duration: animatetime, easing: 'linear', step: function () { $("#month6").children(".large-month-val").children(".val").html(separateNumber( Math.round(this.value))); } }); // Set precision value setTimeout(function() { $("#after-savings-value").html(separateNumber( Math.round(aftervalue / 10) * 10)); $("#month12").children(".large-month-val").children(".val").html(separateNumber(Math.round(aftervalue / 10) * 10)); }, animatetime+20); // Set every month values on chart for (i = 1; i < 12; i++) { $("#month" + i).children(".month-val").children(".val").html(separateNumber(Math.round(monthSavings[i]))); } $("#month6, #month12").children(".month-val").html(""); } // Launch Savings circles if (typeof $("#savings-slider").data('checkedPercent') == 'undefined') { $("#savings-slider").data({"checkedPercent": Number(user.checkedPercent) }); } // Change currency stuff $("#rubcurr, #eurcurr, #usdcurr").removeClass("currchecked"); switch (user.checkedCurr) { case "RUB": $(".curr").html(" Rub ").data("curr", " rub."); $(".savings-circle-currency").html(" Rubles "); $("#rubcurr").addClass("currchecked"); break; case "EUR": $(".curr, .savings-circle-currency").html(" Eur ").data("curr", " \u20ac"); $("#eurcurr").addClass("currchecked"); break; case "USD": $(".curr, .savings-circle-currency").html(" USD ").data("curr", " $"); $("#usdcurr").addClass("currchecked"); break; } if (incomesSumMonth > expensesSumMonth) { setTimeout(function() { $(".topmaincircletitle").html("Spare"); simpleanimatecircle.call($("#inner-circle"), 0, 100, 1200); $("#outer-circle-value").css({"color": "black"}); $("#incomes-cursor").hide(); $("#expense-cursor").show(); animatecircle.call($("#outer-circle"), 0, Math.round(expensesSumMonth), Math.round(incomesSumMonth));}, 350); } else { setTimeout(function() { $(".topmaincircletitle").html("Loss"); simpleanimatecircle.call($("#inner-circle"), 0, 100, 800); $("#outer-circle-value").css({"color": "#eaa7a7"}); $("#expense-cursor").hide(); $("#incomes-cursor").show(); animatecircle.call($("#outer-circle"), 0, Math.round(incomesSumMonth), Math.round(expensesSumMonth)); }, 350); } } function separateNumber(val) { if (Math.abs (val) > 999) { val = val.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1 '); } else { val = val.toString().replace(/(\.)/g, '$1'); } return val; }; // ANIMATE CIRCLE PROCESS function animatecircle(beforevalue, aftervalue, sum, title) { var before = (100 * beforevalue/sum), after = (100 * aftervalue/sum), R = $(this).data("width") / 2, alpha = (after * 0.02 * Math.PI), // percent to radians n = 10, // margin from circle id = $(this).attr("id"), $this = $(this), ycord, xcord; // Set font size (function() { var fontpx; if (id == "outer-circle") { if (sum-aftervalue < 999999) { fontpx = 48 } else if (sum-aftervalue < 9999999) {fontpx = 37 } else { fontpx = 22 } } else { if (aftervalue < 99999) {fontpx = 31} else if (aftervalue < 999999) { fontpx = 27 } else { fontpx = 18 } } $("#" + id + "-value").css({"font-size": fontpx + "px"}) })(); // Percent value location if (after <= 25 ) { ycord = Math.round( R + (n / 2) - (R * Math.sin(1.57 - alpha)) ); xcord = Math.round( R + (n) + (R * Math.cos(1.57 - alpha)) ); animatetime = 500; } else if (after <= 50 ) { ycord = Math.round( R + (n / 2) + (R * Math.sin(alpha - 1.57)) ); xcord = Math.round( R + (n * 2) + (R * Math.cos(alpha - 1.57)) ); animatetime = 1400; } else if (after <= 75 ) { ycord = Math.round( R - (n * 2) + (R * Math.sin(alpha - 1.57)) ); xcord = Math.round( R - (n * 4) + (R * Math.cos(alpha - 1.57)) ); animatetime = 1500; } else { ycord = Math.round( R + (- n * 3) - (R * Math.sin(1.57 - alpha)) ); xcord = Math.round( R + (- n * 3) + (R * Math.cos(1.57 - alpha)) ); animatetime = 1600; } // Set and show percent value next to cursor $("#" + id + "-percent").hide(); $("#" + id + "-percent").empty().append('' + Math.round(after) + '%'); setTimeout(function () { $("#" + id + "-percent").css({"left": xcord, "top": ycord}).fadeIn(400)}, animatetime-200); // Animate circle with its cursor $({ value: before }).animate({ value: after }, { duration: animatetime, easing: 'swing', step: function () { $this.val(this.value).trigger('change'); $("#" + id + "-cursor").val(this.value).trigger('change'); } }); // Set precision value (function() { if (id == "outer-circle") { $("#" + id + "-value").html(separateNumber( Math.abs(Math.round((sum-aftervalue) / 10) * 10) ) ); } else { $("#" + id + "-value").html( separateNumber(aftervalue) ) $("#" + id + "-title").html(title); } })(); } // Animate without percent moving and value growing (for inner circle) function simpleanimatecircle(before, after, duration) { var $this = $(this); $({ value: before }).animate({ value: after }, { duration: duration, easing: 'swing', step: function () { $this.val(this.value).trigger('change'); } }); } /** * EVENT HANDLERS */ // Currency buttons $(".currunchecked").click(function() { if (this.id == "eurcurr") { user.checkedCurr = "EUR"; } else if (this.id == "usdcurr") { user.checkedCurr = "USD"; } else { user.checkedCurr = "RUB"; } runConvert(); initStatisticPage(); setTimeout(function() { initSavingsCircles($("#savings-slider").data("checkedPercent"), 0.2, savings.freeMoney, savings.freeMoney, 700) }, 100); initCircle("first", $("#first-circle").data("item"), 0); initCircle("second", $("#second-circle").data("item"), 0); initCircle("third", $("#third-circle").data("item"), 0); $('#savings-slider').noUiSlider({ start: (incomesSumMonth-expensesSumMonth) * $("#savings-slider").data("checkedPercent"), step: (incomesSumMonth-expensesSumMonth) / 20, range: { 'min': [ 0 ], 'max': [ Math.abs(incomesSumMonth-expensesSumMonth) ] } }, true); }); $("#outermaindiv, #incomes-cursor, #expense-cursor, .lines-title").click(function() { setTimeout(function() { $("#expenses-lines-container, #incomes-lines-container").toggle(); }, 250); $("#expense-cursor, #incomes-cursor").toggle(300); $(".itemline").removeClass("activeline"); $("#line-0, #line-100").addClass("activeline"); }); $("#expenses-lines-container, #incomes-lines-container").on("hover", ".itemline", function() { $(".itemline").removeClass("activeline"); $(this).addClass("activeline"); }); $("#expenses-lines-container, #incomes-lines-container").on("click", ".itemline", function() { $(".itemline").removeClass("activeline"); $(this).addClass("activeline"); var item = $(this).data("item"); initCircle("first", item, 0); initCircle("second", item, 0); initCircle("third", item, 0); }); $("#firstcirclediv, #secondcirclediv, #thirdcirclediv").click(function() { $("#" + this.id + "flipper").toggleClass("flippedcard"); }); $("#circle-select-1, #circle-select-2, #circle-select-3").on("change", function() { var column, item, circle = this.id, whichCircle; switch (circle) { case "circle-select-1": whichCircle = "first"; break; case "circle-select-2": whichCircle = "second"; break; default: whichCircle = "third"; break; } if ($(this).val() < 100 ) { column = incomes; item = $(this).val(); setTimeout(function() { initCircle( whichCircle, column[item], 0) }, 300); } else { column = expenses; item = $(this).val() - 100; setTimeout(function() { initCircle( whichCircle, column[item], 0) }, 300); } $("#" + circle + "-back").removeClass().addClass( column[item].icon ); }); function initSavingsSlider() { var Link = $.noUiSlider.Link, sub; simpleSeparateNumber = function(val) { if (Math.abs (val) > 999) { val = val.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1 '); } else { val = val.toString().replace(/(\.)/g, '$1'); } return val; }; $('#savings-slider').noUiSlider({ start: (incomesSumMonth-expensesSumMonth) * user.checkedPercent, step: (incomesSumMonth-expensesSumMonth) / 20, range: { 'min': [ 0 ], 'max': [ incomesSumMonth-expensesSumMonth ] }, serialization: { lower: [ new Link({ target: function(value){ $("#savings-slider").data({"checkedPercent": Math.round(value / (incomesSumMonth-expensesSumMonth + 0.001) * 100) / 100}); $(".noUi-handle").attr({ percent: Math.round(value / (incomesSumMonth-expensesSumMonth + 0.001) * 100) + "%" }) }, format: { decimals: 0 } }), new Link({ target: function(value){ $(".noUi-handle").attr({ value: simpleSeparateNumber(Math.round(value / 10 ) * 10) + $(".curr").data("curr")}) }, format: { decimals: 0} }) ] } }); } $('#savings-slider').on('slide', function() { var lastVal = $("#after-savings-value").data("lastvalue"), animatetime; if (lastVal < 99999) animatetime = 200; else animatetime = 700; initSavingsCircles($("#savings-slider").data("checkedPercent"), $("#savings-slider").data("lastpercent"), lastVal, $("#after-savings-value").data("lastmiddlevalue"), animatetime); }); (function(){ var canvas1 = document.getElementById("movingcircle-1"), canvas2 = document.getElementById("movingcircle-2"), canvas3 = document.getElementById("movingcircle-3"), ctx1 = canvas1.getContext('2d'), ctx2 = canvas2.getContext('2d'), ctx3 = canvas3.getContext('2d'), x = canvas1.width / 2, y = canvas1.height / 2, radius = 99, startAngle = 1 * Math.PI, endAngle = 2.2 * Math.PI, counterClockwise = false; ctx1.beginPath(); ctx1.arc(x, y, radius, startAngle, endAngle, counterClockwise); ctx1.lineWidth = 3; ctx1.strokeStyle = "#f2f2f2"; ctx1.stroke(); ctx2.beginPath(); ctx2.arc(x, y, radius, startAngle, endAngle, counterClockwise); ctx2.lineWidth = 3; ctx2.strokeStyle = "#f2f2f2"; ctx2.stroke(); ctx3.beginPath(); ctx3.arc(x, y, radius, startAngle, endAngle, counterClockwise); ctx3.lineWidth = 3; ctx3.strokeStyle = "#f2f2f2"; ctx3.stroke(); })(); (function(){ var canvas = document.getElementById("horizontal"), currentDate = new Date(), currentMonth = currentDate.getMonth(), allMonths = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; if (canvas.getContext) { var ctx = canvas.getContext("2d"); ctx.canvas.width = $("#savingschart").width(); ctx.canvas.height = $("#savingschart").height(); for (i=0; i < 11; i++){ ctx.beginPath(); ctx.moveTo(0, 1 + i * 22); ctx.lineTo($("#savingschart").width(), 1 + i * 22); ctx.strokeStyle = '#e5e5e5'; ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, 0.5 + i * 22); ctx.lineTo($("#savingschart").width(), 0.5 + i * 22); ctx.strokeStyle = '#FFF'; ctx.stroke(); } } for (i = 0, j = currentMonth; i <=12; i++, j++) { if (j == 12) {j = 0} $("#month" + i).children(".month-name").html(allMonths[j]); } })(); function drawChartLine(position) { var canvas = document.getElementById("chartline"); var chartWidth = $("#savingschart").width(), chartHeight = $("#savingschart").height(), segmentWidth = $(".months").width(); if (canvas.getContext){ var ctx = canvas.getContext("2d"); ctx.canvas.width = $("#savingschart").width(); ctx.canvas.height = $("#savingschart").height(); ctx.beginPath(); ctx.moveTo(0, 155); ctx.lineTo($("#savingschart").width(), position); ctx.strokeStyle = '#bed8db'; ctx.stroke(); } for (i = 0, j = 12; i < 12; i++, j--) { $("#month" + j).css({"height": chartHeight - position - i * 7 -((i * segmentWidth * (chartHeight - 153 - position) ) / chartWidth) }); } } ================================================ FILE: gateway/src/main/resources/static/js/launch.js ================================================ var global = { mobileClient: false, savePermit: true, usd: 0, eur: 0 }; /** * Oauth2 */ function requestOauthToken(username, password) { var success = false; $.ajax({ url: 'uaa/oauth/token', datatype: 'json', type: 'post', headers: {'Authorization': 'Basic YnJvd3Nlcjo='}, async: false, data: { scope: 'ui', username: username, password: password, grant_type: 'password' }, success: function (data) { localStorage.setItem('token', data.access_token); success = true; }, error: function () { removeOauthTokenFromStorage(); } }); return success; } function getOauthTokenFromStorage() { return localStorage.getItem('token'); } function removeOauthTokenFromStorage() { return localStorage.removeItem('token'); } /** * Current account */ function getCurrentAccount() { var token = getOauthTokenFromStorage(); var account = null; if (token) { $.ajax({ url: 'accounts/current', datatype: 'json', type: 'get', headers: {'Authorization': 'Bearer ' + token}, async: false, success: function (data) { account = data; }, error: function () { removeOauthTokenFromStorage(); } }); } return account; } $(window).load(function(){ if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) { FastClick.attach(document.body); global.mobileClient = true; } $.getJSON("https://api.exchangeratesapi.io/latest?base=RUB&symbols=EUR,USD", function( data ) { global.eur = 1 / data.rates.EUR; global.usd = 1 / data.rates.USD; }); var account = getCurrentAccount(); if (account) { showGreetingPage(account); } else { showLoginForm(); } }); function showGreetingPage(account) { initAccount(account); var userAvatar = $("").attr("src","images/userpic.jpg"); $(userAvatar).load(function() { setTimeout(initGreetingPage, 500); }); } function showLoginForm() { $("#loginpage").show(); $("#frontloginform").focus(); setTimeout(initialShaking, 700); } ================================================ FILE: gateway/src/main/resources/static/js/lib/extrascripts.js ================================================ /* * autoNumeric.js * @author: Bob Knothe * @author: Sokolov Yura * * Created by Robert J. Knothe on 2010-10-25. Please report any bugs to https://github.com/BobKnothe/autoNumeric * Created by Sokolov Yura on 2010-11-07 * * Copyright (c) 2011 Robert J. Knothe http://www.decorplanit.com/plugin/ * * The MIT License (http://www.opensource.org/licenses/mit-license.php) * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ (function(f){function m(b,a,c){void 0===b.selectionStart?(b.focus(),b=b.createTextRange(),b.collapse(!0),b.moveEnd("character",c),b.moveStart("character",a),b.select()):(b.selectionStart=a,b.selectionEnd=c)}function B(b,a){f.each(a,function(c,d){"function"===typeof d?a[c]=d(b,a,c):"function"===typeof b.autoNumeric[d]&&(a[c]=b.autoNumeric[d](b,a,c))})}function p(b,a){"string"===typeof b[a]&&(b[a]*=1)}function w(b,a){B(b,a);a.oEvent=null;a.tagList="B CAPTION CITE CODE DD DEL DIV DFN DT EM H1 H2 H3 H4 H5 H6 INS KDB LABEL LI OUTPUT P Q S SAMPLE SPAN STRONG TD TH U VAR".split(" "); var c=a.vMax.toString().split("."),d=a.vMin||0===a.vMin?a.vMin.toString().split("."):[];p(a,"vMax");p(a,"vMin");p(a,"mDec");a.allowLeading=!0;a.aNeg=0>a.vMin?"-":"";c[0]=c[0].replace("-","");d[0]=d[0].replace("-","");a.mInt=Math.max(c[0].length,d[0].length,1);if(null===a.mDec){var e=0,g=0;c[1]&&(e=c[1].length);d[1]&&(g=d[1].length);a.mDec=Math.max(e,g)}null===a.altDec&&0a.mInt&&"0"===d[0].charAt(0)&&(d[0]=d[0].slice(1));b=e+d.join(a.aDec)}if(c&&"deny"===a.lZero||c&&"allow"===a.lZero&&!1===a.allowLeading)a="^"+a.aNegRegAutoStrip+"0*(\\d"+("leading"===c?")":"|$)"),a=RegExp(a), b=b.replace(a,"$1$2");return b}function x(b,a,c){if(a&&c){var d=b.split(a);d[1]&&d[1].length>c&&(0d&&-1b&&0b&&-1a.vMax)&&f.error("The value ("+c+") from the 'set' method falls outside of the vMin / vMax range");return c>=a.vMin&&c<=a.vMax}function q(b,a,c){return""===b||b===a.aNeg?"zero"===a.wEmpty?b+ "0":"sign"===a.wEmpty||c?b+a.aSign:b:null}function t(b,a){b=h(b,a);var c=b.replace(",","."),d=q(b,a,!0);if(null!==d)return d;var d="",d=2===a.dGroup?/(\d)((\d)(\d{2}?)+)$/:4===a.dGroup?/(\d)((\d{4}?)+)$/:/(\d)((\d{3}?)+)$/,e=b.split(a.aDec);a.altDec&&1===e.length&&(e=b.split(a.altDec));var g=e[0];if(a.aSep)for(;d.test(g);)g=g.replace(d,"$1"+a.aSep+"$2");0!==a.mDec&&1a.mDec&&(e[1]=e[1].substring(0,a.mDec)),b=g+a.aDec+e[1]):b=g;a.aSign&&(d=-1!==b.indexOf(a.aNeg),b=b.replace(a.aNeg, ""),b="p"===a.pSign?a.aSign+b:b+a.aSign,d&&(b=a.aNeg+b));"set"===a.oEvent&&0>c&&null!==a.nBracket&&(b=negativeBracket(b,a.nBracket,a.oEvent));return b}function u(b,a){b=""===b?"0":b.toString();p(a,"mDec");var c="",d=0,e="",g="boolean"===typeof a.aPad||null===a.aPad?a.aPad?a.mDec:0:+a.aPad,n=function(a){a=a.replace(0===g?/(\.[1-9]*)0*$/:1===g?/(\.\d[1-9]*)0*$/:RegExp("(\\.\\d{"+g+"}[1-9]*)0*$"),"$1");0===g&&(a=a.replace(/\.$/,""));return a};"-"===b.charAt(0)&&(e="-",b=b.replace("-",""));b.match(/^\d/)|| (b="0"+b);"-"===e&&0===+b&&(e="");if(0<+b&&"keep"!==a.lZero||0g?c=n(c):0===f&&0===g&&(c=c.replace(/\.$/,""));return e+c}var c=d+a.mDec,d=+b.charAt(c+1),f=b.substring(0,c+1).split(""),h="."===b.charAt(c)?b.charAt(c-1)%2:b.charAt(c)%2;if(4f[d])break;0e.length&&(d=e.length),this.value=e,this.setPosition(d,!1),!0):!1},signPosition:function(){var b=this.settingsClone,a=b.aSign,c=this.that;if(a){a= a.length;if("p"===b.pSign)return b.aNeg&&c.value&&c.value.charAt(0)===b.aNeg?[1,a+1]:[0,a];b=c.value.length;return[b-a,b]}return[1E3,-1]},expandSelectionOnSign:function(b){var a=this.signPosition(),c=this.selection;c.starta[0]&&((c.starta[1])&&this.value.substring(Math.max(c.start,a[0]),Math.min(c.end,a[1])).match(/^\s*$/)?c.start=a||91<=a&&93>=a||9<=a&&31>=a||8>a&&(0===c||c===a)||144===a||145===a||45===a||(d||e)&&65===a)return!0;if((d||e)&&(67===a||86===a||88===a)){"keydown"===b.type&&this.expandSelectionOnSign();if(86===a||45===a)"keydown"===b.type||"keypress"===b.type?void 0===this.valuePartsBeforePaste&&(this.valuePartsBeforePaste=this.getBeforeAfter()):this.checkPaste();return"keydown"===b.type||"keypress"===b.type||67===a}return d||e?!0:37===a||39===a?(c=this.settingsClone.aSep,d=this.selection.start,e=this.that.value, "keydown"===b.type&&c&&!this.shiftKey&&(37===a&&e.charAt(d-2)===c?this.setPosition(d-1):39===a&&e.charAt(d+1)===c&&this.setPosition(d+1)),!0):34<=a&&40>=a?!0:!1},processAllways:function(){var b;return 8===this.kdCode||46===this.kdCode?(this.selection.length?(this.expandSelectionOnSign(!1),b=this.getBeforeAfterStriped()):(b=this.getBeforeAfterStriped(),8===this.kdCode?b[0]=b[0].substring(0,b[0].length-1):b[1]=b[1].substring(1,b[1].length)),this.setValueParts(b[0],b[1]),!0):!1},processKeypress:function(){var b= this.settingsClone,a=String.fromCharCode(this.which),c=this.getBeforeAfterStriped(),d=c[0],c=c[1];if(a===b.aDec||b.altDec&&a===b.altDec||("."===a||","===a)&&110===this.kdCode){if(!b.mDec||!b.aDec||b.aNeg&&-1=a&&(b.aNeg&&""===d&&-1=b.vMax&&b.vMinb.mInt&&"0"===a[0].charAt(0)&&(a[0]=a[0].slice(1));a[0]=e+a[0]}c=t(this.value,this.settingsClone);d=c.length;if(c){a=a[0].split("");e=0;for(e;e is not supported by autoNumeric()"),this;!1===c.runOnce&&c.aForm&&(a.is("input[type=text], input[type=hidden], input:not([type])")&&(d=!0,""===a[0].value&&"empty"===c.wEmpty&&(a[0].value="",d=!1),""===a[0].value&&"sign"=== c.wEmpty&&(a[0].value=c.aSign,d=!1),d&&a.autoNumeric("set",a.val())),-1!==f.inArray(a.prop("tagName"),c.tagList)&&""!==a.text()&&a.autoNumeric("set",a.text()));c.runOnce=!0;a.is("input[type=text], input[type=hidden], input:not([type])")&&(a.on("keydown.autoNumeric",function(b){e=k(a);if(e.settings.aDec===e.settings.aSep)return f.error("autoNumeric will not function properly when the decimal character aDec: '"+e.settings.aDec+"' and thousand separator aSep: '"+e.settings.aSep+"' are the same character"), this;if(e.that.readOnly)return e.processed=!0;e.init(b);e.settings.oEvent="keydown";if(e.skipAllways(b))return e.processed=!0;if(e.processAllways())return e.processed=!0,e.formatQuick(),b.preventDefault(),!1;e.formatted=!1;return!0}),a.on("keypress.autoNumeric",function(b){var c=k(a),d=c.processed;c.init(b);c.settings.oEvent="keypress";if(c.skipAllways(b))return!0;if(d)return b.preventDefault(),!1;if(c.processAllways()||c.processKeypress())return c.formatQuick(),b.preventDefault(),!1;c.formatted= !1}),a.on("keyup.autoNumeric",function(b){var c=k(a);c.init(b);c.settings.oEvent="keyup";b=c.skipAllways(b);c.kdCode=0;delete c.valuePartsBeforePaste;a[0].value===c.settings.aSign&&("s"===c.settings.pSign?m(this,0,0):m(this,c.settings.aSign.length,c.settings.aSign.length));if(b||""===this.value)return!0;c.formatted||c.formatQuick()}),a.on("focusin.autoNumeric",function(){var b=k(a);b.settingsClone.oEvent="focusin";if(null!==b.settingsClone.nBracket){var c=a.val();a.val(negativeBracket(c,b.settingsClone.nBracket, b.settingsClone.oEvent))}b.inVal=a.val();c=q(b.inVal,b.settingsClone,!0);null!==c&&(a.val(c),"s"===b.settings.pSign?m(this,0,0):m(this,b.settings.aSign.length,b.settings.aSign.length))}),a.on("focusout.autoNumeric",function(){var b=k(a),c=b.settingsClone,d=a.val(),e=d;b.settingsClone.oEvent="focusout";var f="";"allow"===c.lZero&&(c.allowLeading=!1,f="leading");""!==d&&(d=h(d,c,f),null===q(d,c)&&s(d,c,a[0])?(d=r(d,c.aDec,c.aNeg),d=u(d,c),d=z(d,c.aDec,c.aNeg)):d="");f=q(d,c,!1);null===f&&(f=t(d,c)); f!==e&&a.val(f);f!==b.inVal&&(a.change(),delete b.inVal);null!==c.nBracket&&0>a.autoNumeric("get")&&(b.settingsClone.oEvent="focusout",a.val(negativeBracket(a.val(),c.nBracket,c.oEvent)))}))})},destroy:function(){return f(this).each(function(){var b=f(this);b.off(".autoNumeric");b.removeData("autoNumeric")})},update:function(b){return f(this).each(function(){var a=l(f(this)),c=a.data("autoNumeric");if("object"!==typeof c)return f.error("You must initialize autoNumeric('init', {options}) prior to calling the 'update' method"), this;var d=a.autoNumeric("get"),c=f.extend(c,b);k(a,c,!0);if(c.aDec===c.aSep)return f.error("autoNumeric will not function properly when the decimal character aDec: '"+c.aDec+"' and thousand separator aSep: '"+c.aSep+"' are the same character"),this;a.data("autoNumeric",c);if(""!==a.val()||""!==a.text())return a.autoNumeric("set",d)})},set:function(b){return f(this).each(function(){var a=l(f(this)),c=a.data("autoNumeric"),d=b.toString(),e=b.toString();if("object"!==typeof c)return f.error("You must initialize autoNumeric('init', {options}) prior to calling the 'set' method"), this;e!==a.attr("value")&&e!==a.text()||!1!==c.runOnce||(d=d.replace(",","."));e!==a.attr("value")&&"INPUT"===a.prop("tagName")&&!1===c.runOnce&&(d=h(d,c));if(!f.isNumeric(+d))return"";d=y(d,c);c.oEvent="set";c.lastSetValue=d;d.toString();""!==d&&(d=u(d,c));d=z(d,c.aDec,c.aNeg);s(d,c)||(d=u("",c));d=t(d,c);if(a.is("input[type=text], input[type=hidden], input:not([type])"))return a.val(d);if(-1!==f.inArray(a.prop("tagName"),c.tagList))return a.text(d);f.error("The <"+a.prop("tagName")+"> is not supported by autoNumeric()"); return!1})},get:function(){var b=l(f(this)),a=b.data("autoNumeric");if("object"!==typeof a)return f.error("You must initialize autoNumeric('init', {options}) prior to calling the 'get' method"),this;a.oEvent="get";var c="";if(b.is("input[type=text], input[type=hidden], input:not([type])"))c=b.eq(0).val();else if(-1!==f.inArray(b.prop("tagName"),a.tagList))c=b.eq(0).text();else return f.error("The <"+b.prop("tagName")+"> is not supported by autoNumeric()"),!1;if(""===c&&"empty"===a.wEmpty||c===a.aSign&& ("sign"===a.wEmpty||"empty"===a.wEmpty))return"";null!==a.nBracket&&""!==c&&(c=negativeBracket(c,a.nBracket,a.oEvent));if(a.runOnce||!1===a.aForm)c=h(c,a);c=r(c,a.aDec,a.aNeg);0===+c&&"keep"!==a.lZero&&(c="0");return"keep"===a.lZero?c:c=y(c,a)},getString:function(){var b=!1,a=l(f(this)).serialize().split("&"),c=0;for(c;c'+option.eq(i).text()+'';}var selectbox=$(''+'
'+optionText+'
'+''+'
'+''+'
');select.before(selectbox).css({position:'absolute',top:-9999});var divSelect=selectbox.find('div.select');var divText=selectbox.find('div.text');var dropdown=selectbox.find('div.dropdown');var li=dropdown.find('li');var selectHeight=selectbox.outerHeight();if(dropdown.css('left')=='auto')dropdown.css({left:0});if(dropdown.css('top')=='auto')dropdown.css({top:selectHeight});var liHeight=li.outerHeight();var position=dropdown.css('top');dropdown.hide();divSelect.click(function(){var topOffset=selectbox.offset().top;var bottomOffset=$(window).height()-selectHeight-(topOffset-$(window).scrollTop());if(bottomOffset<0||bottomOffsettopOffset-$(window).scrollTop()-20){dropdown.height(Math.floor((topOffset-$(window).scrollTop()-20)/liHeight)*liHeight);}}else if(bottomOffset>liHeight*6){dropdown.height('auto').css({bottom:'auto',top:position});if(dropdown.outerHeight()>bottomOffset-20){dropdown.height(Math.floor((bottomOffset-20)/liHeight)*liHeight);}}$('span.selectbox').css({zIndex:1}).removeClass('focused');selectbox.css({zIndex:2});if(dropdown.is(':hidden')){$('div.dropdown:visible').hide();dropdown.show();}else{dropdown.hide();}return false;});li.hover(function(){$(this).siblings().removeClass('selected');});var selectedText=li.filter('.selected').text();li.filter(':not(.disabled)').click(function(){var liText=$(this).text();if(selectedText!=liText){$(this).addClass('selected sel').siblings().removeClass('selected sel');option.removeAttr('selected').eq($(this).index()).attr('selected',true);selectedText=liText;divText.text(liText);select.change();}dropdown.hide();});dropdown.mouseout(function(){dropdown.find('li.sel').addClass('selected');});select.focus(function(){$('span.selectbox').removeClass('focused');selectbox.addClass('focused');}).keyup(function(){divText.text(option.filter(':selected').text());li.removeClass('selected sel').eq(option.filter(':selected').index()).addClass('selected sel');});$(document).on('click',function(e){if(!$(e.target).parents().hasClass('selectbox')){dropdown.hide().find('li.sel').addClass('selected');selectbox.removeClass('focused');}});}doSelect();select.on('refresh',function(){select.prev().remove();doSelect();})}});}})(jQuery); /*! $.noUiSlider - WTFPL - refreshless.com/nouislider/ */ (function(e){function t(e){throw new RangeError("noUiSlider: "+e)}function n(e,n,r){(e[n]||e[r])&&e[n]===e[r]&&t("(Link) '"+n+"' can't match '"+r+"'.'")}function r(e){return"number"===typeof e&&!isNaN(e)&&isFinite(e)}function i(t){return e.isArray(t)?t:[t]}function s(e,t){e.addClass(t);setTimeout(function(){e.removeClass(t)},300)}function o(e,t){return 100*t/(e[1]-e[0])}function u(e,t){if(t>=e.d.slice(-1)[0])return 100;for(var n=1,r,i,s;t>=e.d[n];)n++;r=e.d[n-1];i=e.d[n];s=e.c[n-1];r=[r,i];return s+o(r,0>r[0]?t+Math.abs(r[0]):t-r[0])/(100/(e.c[n]-s))}function a(e,t){for(var n=1,r;t>=e.c[n];)n++;if(e.m)return r=e.c[n-1],n=e.c[n],t-r>(n-r)/2?n:r;e.h[n-1]?(r=e.h[n-1],n=e.c[n-1]+Math.round((t-e.c[n-1])/r)*r):n=t;return n}function f(r){void 0===r&&(r={});"object"!==typeof r&&t("(Format) 'format' option must be an object.");var i={};e(H).each(function(e,n){void 0===r[n]?i[n]=B[e]:typeof r[n]===typeof B[e]?("decimals"===n&&(0>r[n]||7")[0];else if(u)this.method="val",this.j=document.createElement("input"),this.j.name=i,this.j.type="hidden";else if(a)this.target=!1,this.method=i;else{if(f){if(s&&(h||p)){this.target=i;this.method=s;return}if(!s&&c){this.method="val";this.target=i;this.target.on("change",function(t){t=e(t.target).val();var n=r.q;r.u.val([n?null:t,n?t:null],{link:r})});return}if(!s&&!c){this.method="html";this.target=i;return}}throw new RangeError("Link: Invalid Link.")}}function c(e,n){r(n)||t("'step' is not numeric.");e.h[0]=n}function h(n,i){("object"!==typeof i||e.isArray(i))&&t("'range' is not an object.");e.each(i,function(i,s){var o;"number"===typeof s&&(s=[s]);e.isArray(s)||t("'range' contains invalid value.");o="min"===i?0:"max"===i?100:parseFloat(i);r(o)&&r(s[0])||t("'range' value isn't numeric.");n.c.push(o);n.d.push(s[0]);o?n.h.push(isNaN(s[1])?!1:s[1]):isNaN(s[1])||(n.h[0]=s[1])});e.each(n.h,function(e,t){if(!t)return!0;n.h[e]=o([n.d[e],n.d[e+1]],t)/(100/(n.c[e+1]-n.c[e]))})}function p(n,r){"number"===typeof r&&(r=[r]);(!e.isArray(r)||!r.length||2
").addClass(P[2]),i=["-lower","-upper"];t.dir&&i.reverse();r.children().addClass(P[3]+" "+P[3]+i[n]);return r}function x(t,n){n.j&&(n=new l({target:e(n.j).clone().appendTo(t),method:n.method,format:n.g},!0));return n}function T(e,t){var n,r=[];for(n=0;n").appendTo(n).addClass(P[1])}function L(t,n,r){function i(){return b[["width","height"][n.k]]()}function o(e){var t,n=[g.val()];for(t=0;tr&&(r=a(n,r));r=Math.max(Math.min(parseFloat(r.toFixed(7)),100),0);if(r===y[s])return 1===E.length?!1:r===o||r===u?0:!1;t.css(n.style,r+"%");t.is(":first-child")&&t.toggleClass(P[17],50r&&(s+=Math.abs(r)),100e&&(n=this.b("negative"),r=this.b("negativeBefore"));e=Math.abs(e).toFixed(this.b("decimals")).toString();e=e.split(".");0===parseFloat(e)&&(e[0]="0");this.b("thousand")?(i=t(e[0]).match(/.{1,3}/g),i=t(i.join(t(this.b("thousand"))))):i=e[0];this.b("mark")&&1=e.c[i];)i++;s=e.d[i-1];o=e.d[i];u=e.c[i-1];s=[s,o];t=100/(e.c[i]-u)*(t-u)*(s[1]-s[0])/100+s[0]}this.A=t=this.format(t);if("function"===typeof this.method)this.method.call(this.target[0]||r[0],t,n,r);else this.target[this.method](t,n,r)}};l.prototype.format=function(e){return this.g.C(e)};l.prototype.valueOf=function(e){return this.g.t(e)};e.noUiSlider={Link:l};e.fn.noUiSlider=function(e,t){return(t?O:A).call(this,e)};e.fn.val=function(){var t=Array.prototype.slice.call(arguments,0),n,r,s,o;if(!t.length)return this.hasClass(P[0])?this[0].D():_.apply(this);"object"===typeof t[1]?(n=t[1].set,r=t[1].link,s=t[1].update,o=t[1].animate):!0===t[1]&&(n=!0);return this.each(function(){e(this).hasClass(P[0])?this.F(i(t[0]),n,r,s,o):_.apply(e(this),t)})}})(window.jQuery||window.Zepto); /*!jQuery Knob*/ /* * Downward compatible, touchable dial * * Version: 1.2.8 * * Copyright (c) 2012 Anthony Terrien * Under MIT License (http://www.opensource.org/licenses/mit-license.php) * * Thanks to vor, eskimoblood, spiffistan, FabrizioC */ (function(e){"use strict";var t={},n=Math.max,r=Math.min;t.c={};t.c.d=e(document);t.c.t=function(e){return e.originalEvent.touches.length-1};t.o=function(){var n=this;this.o=null;this.$=null;this.i=null;this.g=null;this.v=null;this.cv=null;this.x=0;this.y=0;this.w=0;this.h=0;this.$c=null;this.c=null;this.t=0;this.isInit=false;this.fgColor=null;this.pColor=null;this.dH=null;this.cH=null;this.eH=null;this.rH=null;this.scale=1;this.relative=false;this.relativeWidth=false;this.relativeHeight=false;this.$div=null;this.run=function(){var t=function(e,t){var r;for(r in t){n.o[r]=t[r]}n._carve().init();n._configure()._draw()};if(this.$.data("kontroled"))return;this.$.data("kontroled",true);this.extend();this.o=e.extend({min:this.$.data("min")!==undefined?this.$.data("min"):0,max:this.$.data("max")!==undefined?this.$.data("max"):100,stopper:true,readOnly:this.$.data("readonly")||this.$.attr("readonly")==="readonly",cursor:this.$.data("cursor")===true&&30||this.$.data("cursor")||0,thickness:this.$.data("thickness")&&Math.max(Math.min(this.$.data("thickness"),1),.01)||.35,lineCap:this.$.data("linecap")||"butt",width:this.$.data("width")||200,height:this.$.data("height")||200,displayInput:this.$.data("displayinput")==null||this.$.data("displayinput"),displayPrevious:this.$.data("displayprevious"),fgColor:this.$.data("fgcolor")||"#87CEEB",inputColor:this.$.data("inputcolor"),font:this.$.data("font")||"Arial",fontWeight:this.$.data("font-weight")||"bold",inline:false,step:this.$.data("step")||1,rotation:this.$.data("rotation"),draw:null,change:null,cancel:null,release:null,format:function(e){return e},parse:function(e){return parseFloat(e)}},this.o);this.o.flip=this.o.rotation==="anticlockwise"||this.o.rotation==="acw";if(!this.o.inputColor){this.o.inputColor=this.o.fgColor}if(this.$.is("fieldset")){this.v={};this.i=this.$.find("input");this.i.each(function(t){var r=e(this);n.i[t]=r;n.v[t]=n.o.parse(r.val());r.bind("change blur",function(){var e={};e[t]=r.val();n.val(e)})});this.$.find("legend").remove()}else{this.i=this.$;this.v=this.o.parse(this.$.val());this.v===""&&(this.v=this.o.min);this.$.bind("change blur",function(){n.val(n._validate(n.o.parse(n.$.val())))})}!this.o.displayInput&&this.$.hide();this.$c=e(document.createElement("canvas")).attr({width:this.o.width,height:this.o.height});this.$div=e('
');this.$.wrap(this.$div).before(this.$c);this.$div=this.$.parent();if(typeof G_vmlCanvasManager!=="undefined"){G_vmlCanvasManager.initElement(this.$c[0])}this.c=this.$c[0].getContext?this.$c[0].getContext("2d"):null;if(!this.c){throw{name:"CanvasNotSupportedException",message:"Canvas not supported. Please use excanvas on IE8.0.",toString:function(){return this.name+": "+this.message}}}this.scale=(window.devicePixelRatio||1)/(this.c.webkitBackingStorePixelRatio||this.c.mozBackingStorePixelRatio||this.c.msBackingStorePixelRatio||this.c.oBackingStorePixelRatio||this.c.backingStorePixelRatio||1);this.relativeWidth=this.o.width%1!==0&&this.o.width.indexOf("%");this.relativeHeight=this.o.height%1!==0&&this.o.height.indexOf("%");this.relative=this.relativeWidth||this.relativeHeight;this._carve();if(this.v instanceof Object){this.cv={};this.copy(this.v,this.cv)}else{this.cv=this.v}this.$.bind("configure",t).parent().bind("configure",t);this._listen()._configure()._xy().init();this.isInit=true;this.$.val(this.o.format(this.v));this._draw();return this};this._carve=function(){if(this.relative){var e=this.relativeWidth?this.$div.parent().width()*parseInt(this.o.width)/100:this.$div.parent().width(),t=this.relativeHeight?this.$div.parent().height()*parseInt(this.o.height)/100:this.$div.parent().height();this.w=this.h=Math.min(e,t)}else{this.w=this.o.width;this.h=this.o.height}this.$div.css({width:this.w+"px",height:this.h+"px"});this.$c.attr({width:this.w,height:this.h});if(this.scale!==1){this.$c[0].width=this.$c[0].width*this.scale;this.$c[0].height=this.$c[0].height*this.scale;this.$c.width(this.w);this.$c.height(this.h)}return this};this._draw=function(){var e=true;n.g=n.c;n.clear();n.dH&&(e=n.dH());e!==false&&n.draw()};this._touch=function(e){var r=function(e){var t=n.xy2val(e.originalEvent.touches[n.t].pageX,e.originalEvent.touches[n.t].pageY);if(t==n.cv)return;if(n.cH&&n.cH(t)===false)return;n.change(n._validate(t));n._draw()};this.t=t.c.t(e);r(e);t.c.d.bind("touchmove.k",r).bind("touchend.k",function(){t.c.d.unbind("touchmove.k touchend.k");n.val(n.cv)});return this};this._mouse=function(e){var r=function(e){var t=n.xy2val(e.pageX,e.pageY);if(t==n.cv)return;if(n.cH&&n.cH(t)===false)return;n.change(n._validate(t));n._draw()};r(e);t.c.d.bind("mousemove.k",r).bind("keyup.k",function(e){if(e.keyCode===27){t.c.d.unbind("mouseup.k mousemove.k keyup.k");if(n.eH&&n.eH()===false)return;n.cancel()}}).bind("mouseup.k",function(e){t.c.d.unbind("mousemove.k mouseup.k keyup.k");n.val(n.cv)});return this};this._xy=function(){var e=this.$c.offset();this.x=e.left;this.y=e.top;return this};this._listen=function(){if(!this.o.readOnly){this.$c.bind("mousedown",function(e){e.preventDefault();n._xy()._mouse(e)}).bind("touchstart",function(e){e.preventDefault();n._xy()._touch(e)});this.listen()}else{this.$.attr("readonly","readonly")}if(this.relative){e(window).resize(function(){n._carve().init();n._draw()})}return this};this._configure=function(){if(this.o.draw)this.dH=this.o.draw;if(this.o.change)this.cH=this.o.change;if(this.o.cancel)this.eH=this.o.cancel;if(this.o.release)this.rH=this.o.release;if(this.o.displayPrevious){this.pColor=this.h2rgba(this.o.fgColor,"0.4");this.fgColor=this.h2rgba(this.o.fgColor,"0.6")}else{this.fgColor=this.o.fgColor}return this};this._clear=function(){this.$c[0].width=this.$c[0].width};this._validate=function(e){return~~((e<0?-.5:.5)+e/this.o.step)*this.o.step};this.listen=function(){};this.extend=function(){};this.init=function(){};this.change=function(e){};this.val=function(e){};this.xy2val=function(e,t){};this.draw=function(){};this.clear=function(){this._clear()};this.h2rgba=function(e,t){var n;e=e.substring(1,7);n=[parseInt(e.substring(0,2),16),parseInt(e.substring(2,4),16),parseInt(e.substring(4,6),16)];return"rgba("+n[0]+","+n[1]+","+n[2]+","+t+")"};this.copy=function(e,t){for(var n in e){t[n]=e[n]}}};t.Dial=function(){t.o.call(this);this.startAngle=null;this.xy=null;this.radius=null;this.lineWidth=null;this.cursorExt=null;this.w2=null;this.PI2=2*Math.PI;this.extend=function(){this.o=e.extend({bgColor:this.$.data("bgcolor")||"#FFFFFF",angleOffset:this.$.data("angleoffset")||0,angleArc:this.$.data("anglearc")||360,inline:true},this.o)};this.val=function(e,t){if(null!=e){e=this.o.parse(e);if(t!==false&&e!=this.v&&this.rH&&this.rH(e)===false)return;this.cv=this.o.stopper?n(r(e,this.o.max),this.o.min):e;this.v=this.cv;this.$.val(this.o.format(this.v));this._draw()}else{return this.v}};this.xy2val=function(e,t){var i,s;i=Math.atan2(e-(this.x+this.w2),-(t-this.y-this.w2))-this.angleOffset;if(this.o.flip){i=this.angleArc-i-this.PI2}if(this.angleArc!=this.PI2&&i<0&&i>-.5){i=0}else if(i<0){i+=this.PI2}s=~~(.5+i*(this.o.max-this.o.min)/this.angleArc)+this.o.min;this.o.stopper&&(s=n(r(s,this.o.max),this.o.min));return s};this.listen=function(){var t=this,i,s,o=function(e){e.preventDefault();var o=e.originalEvent,u=o.detail||o.wheelDeltaX,a=o.detail||o.wheelDeltaY,f=t._validate(t.o.parse(t.$.val()))+(u>0||a>0?t.o.step:u<0||a<0?-t.o.step:0);f=n(r(f,t.o.max),t.o.min);t.val(f,false);if(t.rH){clearTimeout(i);i=setTimeout(function(){t.rH(f);i=null},100);if(!s){s=setTimeout(function(){if(i)t.rH(f);s=null},200)}}},u,a,f=1,l={37:-t.o.step,38:t.o.step,39:t.o.step,40:-t.o.step};this.$.bind("keydown",function(i){var s=i.keyCode;if(s>=96&&s<=105){s=i.keyCode=s-48}u=parseInt(String.fromCharCode(s));if(isNaN(u)){s!==13&&s!==8&&s!==9&&s!==189&&(s!==190||t.$.val().match(/\./))&&i.preventDefault();if(e.inArray(s,[37,38,39,40])>-1){i.preventDefault();var o=t.o.parse(t.$.val())+l[s]*f;t.o.stopper&&(o=n(r(o,t.o.max),t.o.min));t.change(o);t._draw();a=window.setTimeout(function(){f*=2},30)}}}).bind("keyup",function(e){if(isNaN(u)){if(a){window.clearTimeout(a);a=null;f=1;t.val(t.$.val())}}else{t.$.val()>t.o.max&&t.$.val(t.o.max)||t.$.val()this.o.max)this.v=this.o.min;this.$.val(this.v);this.w2=this.w/2;this.cursorExt=this.o.cursor/100;this.xy=this.w2*this.scale;this.lineWidth=this.xy*this.o.thickness;this.lineCap=this.o.lineCap;this.radius=this.xy-this.lineWidth/2;this.o.angleOffset&&(this.o.angleOffset=isNaN(this.o.angleOffset)?0:this.o.angleOffset);this.o.angleArc&&(this.o.angleArc=isNaN(this.o.angleArc)?this.PI2:this.o.angleArc);this.angleOffset=this.o.angleOffset*Math.PI/180;this.angleArc=this.o.angleArc*Math.PI/180;this.startAngle=1.5*Math.PI+this.angleOffset;this.endAngle=1.5*Math.PI+this.angleOffset+this.angleArc;var e=n(String(Math.abs(this.o.max)).length,String(Math.abs(this.o.min)).length,2)+2;this.o.displayInput&&this.i.css({width:(this.w/2+4>>0)+"px",height:(this.w/3>>0)+"px",position:"absolute","vertical-align":"middle","margin-top":(this.w/3>>0)+"px","margin-left":"-"+(this.w*3/4+2>>0)+"px",border:0,background:"none",font:this.o.fontWeight+" "+(this.w/e>>0)+"px "+this.o.font,"text-align":"center",color:this.o.inputColor||this.o.fgColor,padding:"0px","-webkit-appearance":"none"})||this.i.css({width:"0px",visibility:"hidden"})};this.change=function(e){this.cv=e;this.$.val(this.o.format(e))};this.angle=function(e){return(e-this.o.min)*this.angleArc/(this.o.max-this.o.min)};this.arc=function(e){var t,n;e=this.angle(e);if(this.o.flip){t=this.endAngle+1e-5;n=t-e-1e-5}else{t=this.startAngle-1e-5;n=t+e+1e-5}this.o.cursor&&(t=n-this.cursorExt)&&(n=n+this.cursorExt);return{s:t,e:n,d:this.o.flip&&!this.o.cursor}};this.draw=function(){var e=this.g,t=this.arc(this.cv),n,r=1;e.lineWidth=this.lineWidth;e.lineCap=this.lineCap;if(this.o.displayPrevious){n=this.arc(this.v);e.beginPath();e.strokeStyle=this.pColor;e.arc(this.xy,this.xy,this.radius,n.s,n.e,n.d);e.stroke();r=this.cv==this.v}e.beginPath();e.strokeStyle=r?this.o.fgColor:this.fgColor;e.arc(this.xy,this.xy,this.radius,t.s,t.e,t.d);e.stroke()};this.cancel=function(){this.val(this.v)}};e.fn.dial=e.fn.knob=function(n){return this.each(function(){var r=new t.Dial;r.o=n;r.$=e(this);r.run()}).parent()}})(jQuery); /* * jQuery Form Plugin; v20131228 * http://jquery.malsup.com/form/ * Copyright (c) 2013 M. Alsup; Dual licensed: MIT/GPL * https://github.com/malsup/form#copyright-and-license */ ;!function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):a("undefined"!=typeof jQuery?jQuery:window.Zepto)}(function(a){"use strict";function b(b){var c=b.data;b.isDefaultPrevented()||(b.preventDefault(),a(b.target).ajaxSubmit(c))}function c(b){var c=b.target,d=a(c);if(!d.is("[type=submit],[type=image]")){var e=d.closest("[type=submit]");if(0===e.length)return;c=e[0]}var f=this;if(f.clk=c,"image"==c.type)if(void 0!==b.offsetX)f.clk_x=b.offsetX,f.clk_y=b.offsetY;else if("function"==typeof a.fn.offset){var g=d.offset();f.clk_x=b.pageX-g.left,f.clk_y=b.pageY-g.top}else f.clk_x=b.pageX-c.offsetLeft,f.clk_y=b.pageY-c.offsetTop;setTimeout(function(){f.clk=f.clk_x=f.clk_y=null},100)}function d(){if(a.fn.ajaxSubmit.debug){var b="[jquery.form] "+Array.prototype.join.call(arguments,"");window.console&&window.console.log?window.console.log(b):window.opera&&window.opera.postError&&window.opera.postError(b)}}var e={};e.fileapi=void 0!==a("").get(0).files,e.formdata=void 0!==window.FormData;var f=!!a.fn.prop;a.fn.attr2=function(){if(!f)return this.attr.apply(this,arguments);var a=this.prop.apply(this,arguments);return a&&a.jquery||"string"==typeof a?a:this.attr.apply(this,arguments)},a.fn.ajaxSubmit=function(b){function c(c){var d,e,f=a.param(c,b.traditional).split("&"),g=f.length,h=[];for(d=0;g>d;d++)f[d]=f[d].replace(/\+/g," "),e=f[d].split("="),h.push([decodeURIComponent(e[0]),decodeURIComponent(e[1])]);return h}function g(d){for(var e=new FormData,f=0;f').val(m.extraData[n].value).appendTo(x)[0]):k.push(a('').val(m.extraData[n]).appendTo(x)[0]));m.iframeTarget||q.appendTo("body"),r.attachEvent?r.attachEvent("onload",h):r.addEventListener("load",h,!1),setTimeout(b,15);try{x.submit()}catch(p){var s=document.createElement("form").submit;s.apply(x)}}finally{x.setAttribute("action",f),x.setAttribute("enctype",j),c?x.setAttribute("target",c):l.removeAttr("target"),a(k).remove()}}function h(b){if(!s.aborted&&!F){if(E=e(r),E||(d("cannot access response document"),b=A),b===z&&s)return s.abort("timeout"),y.reject(s,"timeout"),void 0;if(b==A&&s)return s.abort("server abort"),y.reject(s,"error","server abort"),void 0;if(E&&E.location.href!=m.iframeSrc||v){r.detachEvent?r.detachEvent("onload",h):r.removeEventListener("load",h,!1);var c,f="success";try{if(v)throw"timeout";var g="xml"==m.dataType||E.XMLDocument||a.isXMLDoc(E);if(d("isXml="+g),!g&&window.opera&&(null===E.body||!E.body.innerHTML)&&--G)return d("requeing onLoad callback, DOM not available"),setTimeout(h,250),void 0;var i=E.body?E.body:E.documentElement;s.responseText=i?i.innerHTML:null,s.responseXML=E.XMLDocument?E.XMLDocument:E,g&&(m.dataType="xml"),s.getResponseHeader=function(a){var b={"content-type":m.dataType};return b[a.toLowerCase()]},i&&(s.status=Number(i.getAttribute("status"))||s.status,s.statusText=i.getAttribute("statusText")||s.statusText);var j=(m.dataType||"").toLowerCase(),k=/(json|script|text)/.test(j);if(k||m.textarea){var l=E.getElementsByTagName("textarea")[0];if(l)s.responseText=l.value,s.status=Number(l.getAttribute("status"))||s.status,s.statusText=l.getAttribute("statusText")||s.statusText;else if(k){var o=E.getElementsByTagName("pre")[0],p=E.getElementsByTagName("body")[0];o?s.responseText=o.textContent?o.textContent:o.innerText:p&&(s.responseText=p.textContent?p.textContent:p.innerText)}}else"xml"==j&&!s.responseXML&&s.responseText&&(s.responseXML=H(s.responseText));try{D=J(s,j,m)}catch(t){f="parsererror",s.error=c=t||f}}catch(t){d("error caught: ",t),f="error",s.error=c=t||f}s.aborted&&(d("upload aborted"),f=null),s.status&&(f=s.status>=200&&s.status<300||304===s.status?"success":"error"),"success"===f?(m.success&&m.success.call(m.context,D,"success",s),y.resolve(s.responseText,"success",s),n&&a.event.trigger("ajaxSuccess",[s,m])):f&&(void 0===c&&(c=s.statusText),m.error&&m.error.call(m.context,s,f,c),y.reject(s,"error",c),n&&a.event.trigger("ajaxError",[s,m,c])),n&&a.event.trigger("ajaxComplete",[s,m]),n&&!--a.active&&a.event.trigger("ajaxStop"),m.complete&&m.complete.call(m.context,s,f),F=!0,m.timeout&&clearTimeout(w),setTimeout(function(){m.iframeTarget?q.attr("src",m.iframeSrc):q.remove(),s.responseXML=null},100)}}}var j,k,m,n,o,q,r,s,t,u,v,w,x=l[0],y=a.Deferred();if(y.abort=function(a){s.abort(a)},c)for(k=0;k'),q.css({position:"absolute",top:"-1000px",left:"-1000px"})),r=q[0],s={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(b){var c="timeout"===b?"timeout":"aborted";d("aborting upload... "+c),this.aborted=1;try{r.contentWindow.document.execCommand&&r.contentWindow.document.execCommand("Stop")}catch(e){}q.attr("src",m.iframeSrc),s.error=c,m.error&&m.error.call(m.context,s,c,b),n&&a.event.trigger("ajaxError",[s,m,c]),m.complete&&m.complete.call(m.context,s,c)}},n=m.global,n&&0===a.active++&&a.event.trigger("ajaxStart"),n&&a.event.trigger("ajaxSend",[s,m]),m.beforeSend&&m.beforeSend.call(m.context,s,m)===!1)return m.global&&a.active--,y.reject(),y;if(s.aborted)return y.reject(),y;t=x.clk,t&&(u=t.name,u&&!t.disabled&&(m.extraData=m.extraData||{},m.extraData[u]=t.value,"image"==t.type&&(m.extraData[u+".x"]=x.clk_x,m.extraData[u+".y"]=x.clk_y)));var z=1,A=2,B=a("meta[name=csrf-token]").attr("content"),C=a("meta[name=csrf-param]").attr("content");C&&B&&(m.extraData=m.extraData||{},m.extraData[C]=B),m.forceSync?g():setTimeout(g,10);var D,E,F,G=50,H=a.parseXML||function(a,b){return window.ActiveXObject?(b=new ActiveXObject("Microsoft.XMLDOM"),b.async="false",b.loadXML(a)):b=(new DOMParser).parseFromString(a,"text/xml"),b&&b.documentElement&&"parsererror"!=b.documentElement.nodeName?b:null},I=a.parseJSON||function(a){return window.eval("("+a+")")},J=function(b,c,d){var e=b.getResponseHeader("content-type")||"",f="xml"===c||!c&&e.indexOf("xml")>=0,g=f?b.responseXML:b.responseText;return f&&"parsererror"===g.documentElement.nodeName&&a.error&&a.error("parsererror"),d&&d.dataFilter&&(g=d.dataFilter(g,c)),"string"==typeof g&&("json"===c||!c&&e.indexOf("json")>=0?g=I(g):("script"===c||!c&&e.indexOf("javascript")>=0)&&a.globalEval(g)),g};return y}if(!this.length)return d("ajaxSubmit: skipping submit process - no element selected"),this;var i,j,k,l=this;"function"==typeof b?b={success:b}:void 0===b&&(b={}),i=b.type||this.attr2("method"),j=b.url||this.attr2("action"),k="string"==typeof j?a.trim(j):"",k=k||window.location.href||"",k&&(k=(k.match(/^([^#]+)/)||[])[1]),b=a.extend(!0,{url:k,success:a.ajaxSettings.success,type:i||a.ajaxSettings.type,iframeSrc:/^https/i.test(window.location.href||"")?"javascript:false":"about:blank"},b);var m={};if(this.trigger("form-pre-serialize",[this,b,m]),m.veto)return d("ajaxSubmit: submit vetoed via form-pre-serialize trigger"),this;if(b.beforeSerialize&&b.beforeSerialize(this,b)===!1)return d("ajaxSubmit: submit aborted via beforeSerialize callback"),this;var n=b.traditional;void 0===n&&(n=a.ajaxSettings.traditional);var o,p=[],q=this.formToArray(b.semantic,p);if(b.data&&(b.extraData=b.data,o=a.param(b.data,n)),b.beforeSubmit&&b.beforeSubmit(q,this,b)===!1)return d("ajaxSubmit: submit aborted via beforeSubmit callback"),this;if(this.trigger("form-submit-validate",[q,this,b,m]),m.veto)return d("ajaxSubmit: submit vetoed via form-submit-validate trigger"),this;var r=a.param(q,n);o&&(r=r?r+"&"+o:o),"GET"==b.type.toUpperCase()?(b.url+=(b.url.indexOf("?")>=0?"&":"?")+r,b.data=null):b.data=r;var s=[];if(b.resetForm&&s.push(function(){l.resetForm()}),b.clearForm&&s.push(function(){l.clearForm(b.includeHidden)}),!b.dataType&&b.target){var t=b.success||function(){};s.push(function(c){var d=b.replaceTarget?"replaceWith":"html";a(b.target)[d](c).each(t,arguments)})}else b.success&&s.push(b.success);if(b.success=function(a,c,d){for(var e=b.context||this,f=0,g=s.length;g>f;f++)s[f].apply(e,[a,c,d||l,l])},b.error){var u=b.error;b.error=function(a,c,d){var e=b.context||this;u.apply(e,[a,c,d,l])}}if(b.complete){var v=b.complete;b.complete=function(a,c){var d=b.context||this;v.apply(d,[a,c,l])}}var w=a("input[type=file]:enabled",this).filter(function(){return""!==a(this).val()}),x=w.length>0,y="multipart/form-data",z=l.attr("enctype")==y||l.attr("encoding")==y,A=e.fileapi&&e.formdata;d("fileAPI :"+A);var B,C=(x||z)&&!A;b.iframe!==!1&&(b.iframe||C)?b.closeKeepAlive?a.get(b.closeKeepAlive,function(){B=h(q)}):B=h(q):B=(x||z)&&A?g(q):a.ajax(b),l.removeData("jqxhr").data("jqxhr",B);for(var D=0;Dj;j++)if(n=i[j],l=n.name,l&&!n.disabled)if(b&&g.clk&&"image"==n.type)g.clk==n&&(d.push({name:l,value:a(n).val(),type:n.type}),d.push({name:l+".x",value:g.clk_x},{name:l+".y",value:g.clk_y}));else if(m=a.fieldValue(n,!0),m&&m.constructor==Array)for(c&&c.push(n),k=0,p=m.length;p>k;k++)d.push({name:l,value:m[k]});else if(e.fileapi&&"file"==n.type){c&&c.push(n);var q=n.files;if(q.length)for(k=0;kf;f++)c.push({name:d,value:e[f]});else null!==e&&"undefined"!=typeof e&&c.push({name:this.name,value:e})}}),a.param(c)},a.fn.fieldValue=function(b){for(var c=[],d=0,e=this.length;e>d;d++){var f=this[d],g=a.fieldValue(f,b);null===g||"undefined"==typeof g||g.constructor==Array&&!g.length||(g.constructor==Array?a.merge(c,g):c.push(g))}return c},a.fieldValue=function(b,c){var d=b.name,e=b.type,f=b.tagName.toLowerCase();if(void 0===c&&(c=!0),c&&(!d||b.disabled||"reset"==e||"button"==e||("checkbox"==e||"radio"==e)&&!b.checked||("submit"==e||"image"==e)&&b.form&&b.form.clk!=b||"select"==f&&-1==b.selectedIndex))return null;if("select"==f){var g=b.selectedIndex;if(0>g)return null;for(var h=[],i=b.options,j="select-one"==e,k=j?g+1:i.length,l=j?g:0;k>l;l++){var m=i[l];if(m.selected){var n=m.value;if(n||(n=m.attributes&&m.attributes.value&&!m.attributes.value.specified?m.text:m.value),j)return n;h.push(n)}}return h}return a(b).val()},a.fn.clearForm=function(b){return this.each(function(){a("input,select,textarea",this).clearFields(b)})},a.fn.clearFields=a.fn.clearInputs=function(b){var c=/^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i;return this.each(function(){var d=this.type,e=this.tagName.toLowerCase();c.test(d)||"textarea"==e?this.value="":"checkbox"==d||"radio"==d?this.checked=!1:"select"==e?this.selectedIndex=-1:"file"==d?/MSIE/.test(navigator.userAgent)?a(this).replaceWith(a(this).clone(!0)):a(this).val(""):b&&(b===!0&&/hidden/.test(d)||"string"==typeof b&&a(this).is(b))&&(this.value="")})},a.fn.resetForm=function(){return this.each(function(){("function"==typeof this.reset||"object"==typeof this.reset&&!this.reset.nodeType)&&this.reset()})},a.fn.enable=function(a){return void 0===a&&(a=!0),this.each(function(){this.disabled=!a})},a.fn.selected=function(b){return void 0===b&&(b=!0),this.each(function(){var c=this.type;if("checkbox"==c||"radio"==c)this.checked=b;else if("option"==this.tagName.toLowerCase()){var d=a(this).parent("select");b&&d[0]&&"select-one"==d[0].type&&d.find("option").selected(!1),this.selected=b}})},a.fn.ajaxSubmit.debug=!1}); // PutCursorAtEnd (function(e){jQuery.fn.putCursorAtEnd=function(){return this.each(function(){e(this).focus();if(this.setSelectionRange){var t=e(this).val().length*2;this.setSelectionRange(t,t)}else{e(this).val(e(this).val())}})}})(jQuery); ================================================ FILE: gateway/src/main/resources/static/js/lib/touchscreens.js ================================================ /* * jQuery.fastClick.js * * Work around the 300ms delay for the click event in some mobile browsers. * * @license MIT * @author Dave Hulbert (dave1010) * @version 1.0.0 2013-01-17 */ function FastClick(a){function b(a,b){return function(){return a.apply(b,arguments)}}var c;this.trackingClick=!1;this.trackingClickStart=0;this.targetElement=null;this.lastTouchIdentifier=this.touchStartY=this.touchStartX=0;this.touchBoundary=10;this.layer=a;FastClick.notNeeded(a)||(deviceIsAndroid&&(a.addEventListener("mouseover",b(this.onMouse,this),!0),a.addEventListener("mousedown",b(this.onMouse,this),!0),a.addEventListener("mouseup",b(this.onMouse,this),!0)),a.addEventListener("click",b(this.onClick, this),!0),a.addEventListener("touchstart",b(this.onTouchStart,this),!1),a.addEventListener("touchmove",b(this.onTouchMove,this),!1),a.addEventListener("touchend",b(this.onTouchEnd,this),!1),a.addEventListener("touchcancel",b(this.onTouchCancel,this),!1),Event.prototype.stopImmediatePropagation||(a.removeEventListener=function(b,c,e){var f=Node.prototype.removeEventListener;"click"===b?f.call(a,b,c.hijacked||c,e):f.call(a,b,c,e)},a.addEventListener=function(b,c,e){var f=Node.prototype.addEventListener; "click"===b?f.call(a,b,c.hijacked||(c.hijacked=function(a){a.propagationStopped||c(a)}),e):f.call(a,b,c,e)}),"function"===typeof a.onclick&&(c=a.onclick,a.addEventListener("click",function(a){c(a)},!1),a.onclick=null))}var deviceIsAndroid=0c.offsetHeight){b=c;a.fastClickScrollParent=c;break}c=c.parentElement}while(c)}b&&(b.fastClickLastScrollTop=b.scrollTop)}; FastClick.prototype.getTargetElementFromEventTarget=function(a){return a.nodeType===Node.TEXT_NODE?a.parentNode:a}; FastClick.prototype.onTouchStart=function(a){var b,c,d;if(1a.timeStamp-this.lastClickTime&&a.preventDefault();return!0};FastClick.prototype.touchHasMoved=function(a){a=a.changedTouches[0];var b=this.touchBoundary;return Math.abs(a.pageX-this.touchStartX)>b||Math.abs(a.pageY-this.touchStartY)>b?!0:!1};FastClick.prototype.onTouchMove=function(a){if(!this.trackingClick)return!0;if(this.targetElement!==this.getTargetElementFromEventTarget(a.target)||this.touchHasMoved(a))this.trackingClick=!1,this.targetElement=null;return!0}; FastClick.prototype.findControl=function(a){return void 0!==a.control?a.control:a.htmlFor?document.getElementById(a.htmlFor):a.querySelector("button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea")}; FastClick.prototype.onTouchEnd=function(a){var b,c,d=this.targetElement;if(!this.trackingClick)return!0;if(200>a.timeStamp-this.lastClickTime)return this.cancelNextClick=!0;this.cancelNextClick=!1;this.lastClickTime=a.timeStamp;b=this.trackingClickStart;this.trackingClick=!1;this.trackingClickStart=0;deviceIsIOSWithBadTarget&&(c=a.changedTouches[0],d=document.elementFromPoint(c.pageX-window.pageXOffset,c.pageY-window.pageYOffset)||d,d.fastClickScrollParent=this.targetElement.fastClickScrollParent); c=d.tagName.toLowerCase();if("label"===c){if(b=this.findControl(d)){this.focus(d);if(deviceIsAndroid)return!1;d=b}}else if(this.needsFocus(d)){if(1000){return}var ba=a9.originalEvent?a9.originalEvent:a9;var a8,a7=a?ba.touches[0]:ba;W=f;if(a){T=ba.touches.length}else{a9.preventDefault()}ac=0;aL=null;aF=null;Y=0;aX=0;aV=0;D=1;am=0;aM=af();J=X();O();if(!a||(T===aq.fingers||aq.fingers===h)||aT()){ae(0,a7);Q=ao();if(T==2){ae(1,ba.touches[1]);aX=aV=ap(aM[0].start,aM[1].start)}if(aq.swipeStatus||aq.pinchStatus){a8=L(ba,W)}}else{a8=false}if(a8===false){W=p;L(ba,W);return a8}else{ak(true)}return null}function aZ(ba){var bd=ba.originalEvent?ba.originalEvent:ba;if(W===g||W===p||ai()){return}var a9,a8=a?bd.touches[0]:bd;var bb=aD(a8);aY=ao();if(a){T=bd.touches.length}W=j;if(T==2){if(aX==0){ae(1,bd.touches[1]);aX=aV=ap(aM[0].start,aM[1].start)}else{aD(bd.touches[1]);aV=ap(aM[0].end,aM[1].end);aF=an(aM[0].end,aM[1].end)}D=a3(aX,aV);am=Math.abs(aX-aV)}if((T===aq.fingers||aq.fingers===h)||!a||aT()){aL=aH(bb.start,bb.end);ah(ba,aL);ac=aO(bb.start,bb.end);Y=aI();aE(aL,ac);if(aq.swipeStatus||aq.pinchStatus){a9=L(bd,W)}if(!aq.triggerOnTouchEnd||aq.triggerOnTouchLeave){var a7=true;if(aq.triggerOnTouchLeave){var bc=aU(this);a7=B(bb.end,bc)}if(!aq.triggerOnTouchEnd&&a7){W=ay(j)}else{if(aq.triggerOnTouchLeave&&!a7){W=ay(g)}}if(W==p||W==g){L(bd,W)}}}else{W=p;L(bd,W)}if(a9===false){W=p;L(bd,W)}}function I(a7){var a8=a7.originalEvent;if(a){if(a8.touches.length>0){C();return true}}if(ai()){T=aa}a7.preventDefault();aY=ao();Y=aI();if(a6()){W=p;L(a8,W)}else{if(aq.triggerOnTouchEnd||(aq.triggerOnTouchEnd==false&&W===j)){W=g;L(a8,W)}else{if(!aq.triggerOnTouchEnd&&a2()){W=g;aB(a8,W,x)}else{if(W===j){W=p;L(a8,W)}}}}ak(false);return null}function a5(){T=0;aY=0;Q=0;aX=0;aV=0;D=1;O();ak(false)}function H(a7){var a8=a7.originalEvent;if(aq.triggerOnTouchLeave){W=ay(g);L(a8,W)}}function aG(){aN.unbind(G,aJ);aN.unbind(az,a5);aN.unbind(au,aZ);aN.unbind(R,I);if(P){aN.unbind(P,H)}ak(false)}function ay(bb){var ba=bb;var a9=aw();var a8=aj();var a7=a6();if(!a9||a7){ba=p}else{if(a8&&bb==j&&(!aq.triggerOnTouchEnd||aq.triggerOnTouchLeave)){ba=g}else{if(!a8&&bb==g&&aq.triggerOnTouchLeave){ba=p}}}return ba}function L(a9,a7){var a8=undefined;if(F()||S()){a8=aB(a9,a7,k)}else{if((M()||aT())&&a8!==false){a8=aB(a9,a7,s)}}if(aC()&&a8!==false){a8=aB(a9,a7,i)}else{if(al()&&a8!==false){a8=aB(a9,a7,b)}else{if(ad()&&a8!==false){a8=aB(a9,a7,x)}}}if(a7===p){a5(a9)}if(a7===g){if(a){if(a9.touches.length==0){a5(a9)}}else{a5(a9)}}return a8}function aB(ba,a7,a9){var a8=undefined;if(a9==k){aN.trigger("swipeStatus",[a7,aL||null,ac||0,Y||0,T]);if(aq.swipeStatus){a8=aq.swipeStatus.call(aN,ba,a7,aL||null,ac||0,Y||0,T);if(a8===false){return false}}if(a7==g&&aR()){aN.trigger("swipe",[aL,ac,Y,T]);if(aq.swipe){a8=aq.swipe.call(aN,ba,aL,ac,Y,T);if(a8===false){return false}}switch(aL){case o:aN.trigger("swipeLeft",[aL,ac,Y,T]);if(aq.swipeLeft){a8=aq.swipeLeft.call(aN,ba,aL,ac,Y,T)}break;case n:aN.trigger("swipeRight",[aL,ac,Y,T]);if(aq.swipeRight){a8=aq.swipeRight.call(aN,ba,aL,ac,Y,T)}break;case d:aN.trigger("swipeUp",[aL,ac,Y,T]);if(aq.swipeUp){a8=aq.swipeUp.call(aN,ba,aL,ac,Y,T)}break;case v:aN.trigger("swipeDown",[aL,ac,Y,T]);if(aq.swipeDown){a8=aq.swipeDown.call(aN,ba,aL,ac,Y,T)}break}}}if(a9==s){aN.trigger("pinchStatus",[a7,aF||null,am||0,Y||0,T,D]);if(aq.pinchStatus){a8=aq.pinchStatus.call(aN,ba,a7,aF||null,am||0,Y||0,T,D);if(a8===false){return false}}if(a7==g&&a4()){switch(aF){case c:aN.trigger("pinchIn",[aF||null,am||0,Y||0,T,D]);if(aq.pinchIn){a8=aq.pinchIn.call(aN,ba,aF||null,am||0,Y||0,T,D)}break;case w:aN.trigger("pinchOut",[aF||null,am||0,Y||0,T,D]);if(aq.pinchOut){a8=aq.pinchOut.call(aN,ba,aF||null,am||0,Y||0,T,D)}break}}}if(a9==x){if(a7===p||a7===g){clearTimeout(aS);if(V()&&!E()){K=ao();aS=setTimeout(e.proxy(function(){K=null;aN.trigger("tap",[ba.target]);if(aq.tap){a8=aq.tap.call(aN,ba,ba.target)}},this),aq.doubleTapThreshold)}else{K=null;aN.trigger("tap",[ba.target]);if(aq.tap){a8=aq.tap.call(aN,ba,ba.target)}}}}else{if(a9==i){if(a7===p||a7===g){clearTimeout(aS);K=null;aN.trigger("doubletap",[ba.target]);if(aq.doubleTap){a8=aq.doubleTap.call(aN,ba,ba.target)}}}else{if(a9==b){if(a7===p||a7===g){clearTimeout(aS);K=null;aN.trigger("longtap",[ba.target]);if(aq.longTap){a8=aq.longTap.call(aN,ba,ba.target)}}}}}return a8}function aj(){var a7=true;if(aq.threshold!==null){a7=ac>=aq.threshold}return a7}function a6(){var a7=false;if(aq.cancelThreshold!==null&&aL!==null){a7=(aP(aL)-ac)>=aq.cancelThreshold}return a7}function ab(){if(aq.pinchThreshold!==null){return am>=aq.pinchThreshold}return true}function aw(){var a7;if(aq.maxTimeThreshold){if(Y>=aq.maxTimeThreshold){a7=false}else{a7=true}}else{a7=true}return a7}function ah(a7,a8){if(aq.allowPageScroll===l||aT()){a7.preventDefault()}else{var a9=aq.allowPageScroll===r;switch(a8){case o:if((aq.swipeLeft&&a9)||(!a9&&aq.allowPageScroll!=A)){a7.preventDefault()}break;case n:if((aq.swipeRight&&a9)||(!a9&&aq.allowPageScroll!=A)){a7.preventDefault()}break;case d:if((aq.swipeUp&&a9)||(!a9&&aq.allowPageScroll!=t)){a7.preventDefault()}break;case v:if((aq.swipeDown&&a9)||(!a9&&aq.allowPageScroll!=t)){a7.preventDefault()}break}}}function a4(){var a8=aK();var a7=U();var a9=ab();return a8&&a7&&a9}function aT(){return !!(aq.pinchStatus||aq.pinchIn||aq.pinchOut)}function M(){return !!(a4()&&aT())}function aR(){var ba=aw();var bc=aj();var a9=aK();var a7=U();var a8=a6();var bb=!a8&&a7&&a9&&bc&&ba;return bb}function S(){return !!(aq.swipe||aq.swipeStatus||aq.swipeLeft||aq.swipeRight||aq.swipeUp||aq.swipeDown)}function F(){return !!(aR()&&S())}function aK(){return((T===aq.fingers||aq.fingers===h)||!a)}function U(){return aM[0].end.x!==0}function a2(){return !!(aq.tap)}function V(){return !!(aq.doubleTap)}function aQ(){return !!(aq.longTap)}function N(){if(K==null){return false}var a7=ao();return(V()&&((a7-K)<=aq.doubleTapThreshold))}function E(){return N()}function at(){return((T===1||!a)&&(isNaN(ac)||ac===0))}function aW(){return((Y>aq.longTapThreshold)&&(ac=0)){return o}else{if((a9<=360)&&(a9>=315)){return o}else{if((a9>=135)&&(a9<=225)){return n}else{if((a9>45)&&(a9<135)){return v}else{return d}}}}}function ao(){var a7=new Date();return a7.getTime()}function aU(a7){a7=e(a7);var a9=a7.offset();var a8={left:a9.left,right:a9.left+a7.outerWidth(),top:a9.top,bottom:a9.top+a7.outerHeight()};return a8}function B(a7,a8){return(a7.x>a8.left&&a7.xa8.top&&a7.y").attr("src","images/userpic.jpg"); $(userAvatar).load(function() { setTimeout(initGreetingPage, 500); }); } else { $("#preloader, #enter, #secondenter").hide(); flipForm(); $('.frontforms').val(''); $("#frontloginform").focus(); alert("Something went wrong. Please, check your credentials"); } } /** * Logout */ function logout() { removeOauthTokenFromStorage(); location.reload(); } /** * Demo */ $(".demobutton").bind("click", function(){ $.ajax({ url: 'accounts/demo', datatype: 'json', type: 'get', async: false, success: function (data) { global.savePermit = false; initAccount(data); var userAvatar = $("").attr("src","images/userpic.jpg"); $(userAvatar).load(function() { setTimeout(initGreetingPage, 500); }); }, error: function () { alert("Something went wrong. Please, try again"); } }); }); $("#skipmail").bind("click", function(){ $("#lastlogo").show(); setTimeout(initGreetingPage, 300); }); /** * Login form effects */ function initialShaking(){ autoShake(); setTimeout(autoShake, 1900); } function autoShake() { $("#piggy").toggleClass("auto-shake"); } function OnHoverShaking() { hoverShake(); setTimeout(hoverShake, 1700); } function hoverShake() { $("#piggy").toggleClass("hover-shake"); } function toggleInfo() { $("#infopage").toggle(); } function flipForm() { $("#cube").toggleClass("flippedform"); $("#frontpasswordform").focus(); } $("#piggy").on("click mouseover", function(){ if ($(this).hasClass("skakelogo") === false && $(this).hasClass("hover-shake") === false) { OnHoverShaking(); } }); $(".fliptext").bind("click", function(){ setTimeout( function() { $("#plusavatar").addClass("avataranimation"); } , 1000); $("#flipper").toggleClass("flippedcard"); }); $(".flipinfo").on("click", function() { $("#flipper").toggleClass("flippedcardinfo"); toggleInfo(); }); $(".frominfo, #infotitle, #infosubtitle").on("click", function() { $("#flipper").toggleClass("flippedcardinfo"); setTimeout(toggleInfo, 400); }); $("#enter").on("click", function() {flipForm()}); $("#secondenter").on("click", function() {login()}); $("#frontloginform").keyup(function (e) { if( $(this).val().length >= 3 ) { $("#enter").show(); if (e.which == 13) { flipForm(); $("#enter").hide(); } return; } else { $("#enter").hide(); } }); $("#frontpasswordform").keyup(function(e) { if ( $(this).val().length >= 6) { $("#secondenter").show(); if(e.which == 13) { $(this).blur(); login(); } return; } else { $("#secondenter").hide(); } }); ================================================ FILE: gateway/src/main/resources/static/js/main.js ================================================ var user = {}, savings = {}, incomes = {}, expenses = {}; function initAccount(account) { user = new User(account.name, account.lastSeen, account.saving.currency, account.note); savings = new Savings (account.saving.amount, account.saving.deposit, account.saving.capitalization, account.saving.interest); if (account.incomes) { for (i = 0; i < account.incomes.length; i++) { AddIncome(i + 1, account.incomes[i].title, account.incomes[i].icon, account.incomes[i].currency, account.incomes[i].period, account.incomes[i].amount); } } if (account.expenses) { for (j = 0; j < account.expenses.length; j++) { AddExpense(j + 1, account.expenses[j].title, account.expenses[j].icon, account.expenses[j].currency, account.expenses[j].period, account.expenses[j].amount); } } } function User(username, lastSeen, currency, note) { var seen = new Date(lastSeen); this.login = username; this.lastSeen = (seen.getMonth() + 1) + "/" + seen.getDate() + "/" + seen.getFullYear();; this.checkedCurr = currency; this.lastCurr = currency; this.checkedPercent = 1; this.notes = note; } function Savings(money, deposit, capitalization, interest) { this.freeMoney = money; this.deposit = deposit; this.capitalization = capitalization; this.percent = interest; } function AddIncome(income_id, title, icon, currency, period, amount){ incomes[income_id] = { income_id: income_id, title: title, icon: icon, currency: currency, period: period, amount: amount.toString() } } function AddExpense(expense_id, title, icon, currency, period, amount){ expenses[expense_id] = { expense_id: expense_id, title: title, icon: icon, currency: currency, period: period, amount: amount.toString() } } // Log out button $('#minus').click(function() { logout(); }); var entityMap = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/' }; function escape(string) { return String(string).replace(/[&<>"'\/]/g, function (s) { return entityMap[s]; }); } function sanitize(obj) { $.each( obj, function( index, item ){ $.each( item, function( key, value ){ obj[index][key] = escape(value); }); }); return obj; } function initGreetingPage() { $("#preloader, #lastlogo").show(); $("#loginpage").fadeOut(50); $(".avatar").css({"background": "url(images/userpic.jpg) center center no-repeat", "background-size": "100% 100%"}); $("#logo_greeting").fadeIn(0, function() { $("#centerbox").show(0,function() { setTimeout( function() { showGreetingUnits() } , 300) }); }); $(".plus").click(function() { setTimeout(initSettingsPage, 200); }); // Init statisctic page first time expensesSumMonth = 0; incomesSumMonth = 0; $("#circle-select-1, #circle-select-2, #circle-select-3").empty(); getConverted(incomes); getConverted(expenses); initStatisticPage(); setTimeout(function() { initSavingsCircles(user.checkedPercent, 0.2, savings.freeMoney, savings.freeMoney, 700) }, 200); initSavingsSlider(); $("#savings-slider").data({"checkedPercent": user.checkedPercent}); $("#lefttitle").prepend(escape(user.login)); $("#righttitle").append(user.lastSeen); // Fill data on settings page beforehand addSavings(); addItems(); addNotes(); } function initSettingsPage() { switch (user.checkedCurr) { case "RUB": $("#rublesign").css({"background-position": "-150px 0"}); break; case "EUR": $("#rublesign").css({"background-position": "-386px 0"}); break; case "USD": $("#rublesign").css({"background-position": "-354px 0"}); break; } $("#settings_hat").show(); $("#avatarcontainer").addClass("reverse"); $("#overlay, .modal-content").show(); // Fix transition and visibility: hidden Chrome issue $(".modalvalue").autoNumeric("init"); $(".modalcurrency, .modalperiod, .selectcircles").selectbox(); hideGreetingUnits(); setTimeout( function() { $("#logo_greeting").hide(); $("#logo_settings").show(); $("#avatarcontainer").fadeOut(100); drawChartLine(91); } , 290); setTimeout( function() { $("#settingspage").fadeIn(100); } , 700); setTimeout( function() { $("#expenseslider").fadeIn(100); $("#bubble").fadeIn(500); } , 800); } function greetingPageAgain() { $("#avatarcontainer").removeClass().addClass("forward plus"); $(".avatar").css({"background": "url(images/userpic.jpg) center center no-repeat", "background-size": "100% 100%"}); $("#logo_greeting").fadeIn(0, function() { $("#centerbox, #avatarcontainer").show(0,function() { setTimeout( function() { showGreetingUnits(); } , 300); }); }); /* Initiate settings page on press plus or enter $(document).on("keyup", function (e) { if (e.which == 13) { setTimeout(initSettingsPage, 400); } }); */ $(".plus").click(function() { setTimeout(initSettingsPage, 200); }); $("#righttitle, #lefttitle").empty(); $("#righttitle").append('last seen: ' + user.lastSeen); $("#lefttitle").append(escape(user.login) + ' metrics'); } function showGreetingUnits() { $("#lefttitle").fadeIn(500); $("#righttitle").fadeIn(500); $("#bottombuttons").fadeIn(500); } function hideGreetingUnits() { $("#lefttitle").fadeOut(100); $("#righttitle").fadeOut(100); $("#bottombuttons").fadeOut(100); } // Filling user notes function addNotes() { $("textarea#notes").val(user.notes); } // Filling Savings column function addSavings() { switch (savings.deposit) { case true: $("#deposit").prop('checked', true); break; } switch (savings.capitalization) { case true: $("#capitalization").prop('checked', true); break; } $("#savingsvalue").val(savings.freeMoney); $("#percentvalue").val(savings.percent); $("#savingsvalue").autoNumeric("init"); $("#percentvalue").autoNumeric("init"); moveRuble(); } // Filling Incomes and Expenses columns function addItems() { Object.keys(incomes).forEach(function(key) { var value = incomes[key].amount.replace(/(\d)(?=(\d\d\d)+([^\d]|$))/g, '$1
'); $("#incomeslider").append('
' + incomes[key].title + '

' + value + ' ' + checkCurrency(incomes[key].currency) + checkPeriod(incomes[key].period) + '

'); $("#income-" + incomes[key].income_id).data({"id": incomes[key].income_id, "icon": incomes[key].icon, "amount": incomes[key].amount, "title": incomes[key].title, "currency": incomes[key].currency ,"period": incomes[key].period}).children("div").addClass(incomes[key].icon); }); Object.keys(expenses).forEach(function(key) { var value = expenses[key].amount.replace(/(\d)(?=(\d\d\d)+([^\d]|$))/g, '$1
'); $("#expenseslider").append('
' + expenses[key].title + '

' + value + ' ' + checkCurrency(expenses[key].currency) + checkPeriod(expenses[key].period) + '

'); $("#expense-" + expenses[key].expense_id).data({"id": expenses[key].expense_id, "icon": expenses[key].icon, "amount": expenses[key].amount, "title": expenses[key].title, "currency": expenses[key].currency ,"period": expenses[key].period}).children("div").addClass(expenses[key].icon); }); // Show big ADD ITEM button when column is empty checkSlidersLength(); // Markup changes according to number of column items itemsPosition("expense"); itemsPosition("income"); } // According to number of column items - show/hide up&down buttons and change position:absolute function itemsPosition(transaction) { if ($("div#" + transaction + "slider").children().length <= 4) { $("#" + transaction + "slider").css({"position": "relative"}); $("#" + transaction + "down, #" + transaction + "up").hide(); } else { $("#" + transaction + "slider").css({"position": "absolute"}); $("#" + transaction + "down, #" + transaction + "up").show(); } } function checkSlidersLength() { switch ($("div#incomeslider").children().length) { case 0: $("#noincomes").show(); $("#incomesplusitem").hide(); break; default: $("#noincomes").hide(); $("#incomesplusitem").show(); break; } switch ($("div#expenseslider").children().length) { case 0: $("#noexpenses").show(); $("#expensesplusitem").hide(); break; default: $("#noexpenses").hide(); $("#expensesplusitem").show(); break; } } function checkPeriod(period) { var periodText; switch (period) { case "YEAR": periodText = " / per year"; break; case "QUARTER": periodText = " / per quater"; break; case "MONTH": periodText = " / per month"; break; case "DAY": periodText = " / per day"; break; case "HOUR": periodText = " / per hour"; break; } return periodText } function checkCurrency(currency) { var currencyText; switch (currency) { case "RUB": currencyText="rub."; break; case "USD": currencyText="$"; break; case "EUR": currencyText="€"; break; } return currencyText } // SLIDE COLUMNS UP AND DOWN // Eliminate the frequent calling slide functions function debounce(f, ms) { var state = null; var cooldown = 1; return function() { if (state) return; f.apply(this, arguments); state = cooldown; setTimeout(function() { state = null }, ms); } } Up = debounce(Up, 300); Down = debounce(Down, 300); initStatisticPage = debounce(initStatisticPage, 900); launchStatistic = debounce(launchStatistic, 2400); fadeStatistic = debounce(fadeStatistic, 1900); jsonDataSave = debounce(jsonDataSave, 1800); startOfExpenseList = debounce(startOfExpenseList, 500); endOfExpenseList = debounce(endOfExpenseList, 500); startOfIncomeList = debounce(startOfIncomeList, 500); endOfIncomeList = debounce(endOfIncomeList, 500); initGreetingPage = debounce(initGreetingPage, 2000); // Slide up function Up(transaction) { var length = $("#" + transaction + "slider").children().length; var itemWidth = 71; if (Math.abs($("#" + transaction + "slider").position().top % itemWidth) > 0.01) return; else if ($("#" + transaction + "slider").position().top > -(length-4)*itemWidth) { var pixels=$("#" + transaction + "slider").position().top + (length-5)*itemWidth; var slide = "translateY("+ pixels + "px)"; $("#" + transaction + "slider").css({ "-webkit-transform": slide, "-moz-transform": slide, "-o-transform": slide, "-ms-transform": slide, "transform": slide }); } else { if (transaction == "expense") { endOfExpenseList(transaction); setTimeout(endOfExpenseList, 500); } else { endOfIncomeList(transaction); setTimeout(endOfIncomeList, 500); } } } // Slide down function Down(transaction) { var length = $("div#" + transaction + "slider").children().length; var itemWidth = 71; if (Math.abs($("#" + transaction + "slider").position().top % itemWidth) > 0.01) return; else if ($("#" + transaction + "slider").position().top < -20) { var pixels=$("#" + transaction + "slider").position().top + (length-3)*itemWidth; var slide = "translateY("+ pixels + "px)"; $("#" + transaction + "slider").css({ "-webkit-transform": slide, "-moz-transform": slide, "-o-transform": slide, "-ms-transform": slide, "transform": slide }); } else { if (transaction == "expense") { startOfExpenseList(transaction); setTimeout(startOfExpenseList, 530); } else { startOfIncomeList(transaction); setTimeout(startOfIncomeList, 530); } } } // Bounce end of list functions function startOfExpenseList(transaction) { $("#expensewrapper").toggleClass("startoflist"); } function endOfExpenseList(transaction) { $("#expensewrapper").toggleClass("endoflist"); } function startOfIncomeList(transaction) { $("#incomewrapper").toggleClass("startoflist"); } function endOfIncomeList(transaction) { $("#incomewrapper").toggleClass("endoflist"); } // MODAL WINDOWS FUNCTIONS // ADD ITEMS function addNewItem() { var itemTitle = $(".modaltitle").val(), itemIcon = $(".initicons").data("iconselected"), itemCurrency = $(".modalcurrency").val(), itemPeriod = $(".modalperiod").val(), itemValue = $(".modalvalue").autoNumeric("get"), itemId = 0; if (checkModalFields(itemValue, itemTitle)) { // If inputs are proper filled, add incomes or expenses if ($(".initicons").data ("incomes-expenses") == "incomes") { Object.keys(incomes).forEach(function(keys) { if (incomes[keys].income_id > itemId) { itemId = (incomes[keys].income_id) }}); AddIncome(++itemId, itemTitle, itemIcon, itemCurrency, itemPeriod, itemValue); addNewDiv("income", itemId, itemTitle, itemIcon, itemCurrency, itemPeriod, itemValue); } else { Object.keys(expenses).forEach(function(keys) { if (expenses[keys].expense_id > itemId) { itemId = (expenses[keys].expense_id) }}); AddExpense(++itemId, itemTitle, itemIcon, itemCurrency, itemPeriod, itemValue); addNewDiv("expense", itemId, itemTitle, itemIcon, itemCurrency, itemPeriod, itemValue); } turnOffModal(); } checkSlidersLength(); itemsPosition("expense"); itemsPosition("income"); setTimeout(function() { runConvert() }, 230); } function addNewDiv(whichColumn, itemId, itemTitle, itemIcon, itemCurrency, itemPeriod, itemValue) { var value = itemValue.replace(/(\d)(?=(\d\d\d)+([^\d]|$))/g, '$1
'); $("#" + whichColumn + "slider").append('
' + itemTitle + '

' + value + ' ' + checkCurrency(itemCurrency) + checkPeriod(itemPeriod) + '

'); $("#" + whichColumn + "-" + itemId).addClass("newitemadded").data({"id": itemId, "icon": itemIcon, "amount": itemValue, "title": itemTitle, "currency": itemCurrency ,"period": itemPeriod}).children("div").addClass(itemIcon); setTimeout(function() { $("#" + whichColumn + "-" + itemId).removeClass("newitemadded") }, 4100); } // EDIT ITEMS function itemClick(item) { // Add delete-button $(".modal-delete").show(); var itemDiv = $("#" + item.id), itemIcon = itemDiv.data("icon"), itemCurrency = itemDiv.data("currency"), itemPeriod = itemDiv.data("period"), itemValue = itemDiv.data("amount"), itemTitle = itemDiv.data("title"), itemId = itemDiv.data("id"), incomesExpenses = "expense", whichColumn = expenses; if (itemDiv.hasClass("incomeitem")) { $(".mainmodaltitle").empty().append("Change income"); incomesExpenses = "income"; whichColumn = incomes; } else { $(".mainmodaltitle").empty().append("Change expense"); } $(".initicons").data({"iconselected": itemIcon, "add-edit": "edit", "incomes-expenses": incomesExpenses + "s"}); $("#chooseicon").removeClass().addClass(itemIcon); $(".modalvalue").show().autoNumeric("set", itemValue); $(".modaltitle").show().val(itemTitle); $(".modalcurrency").val(itemCurrency); $(".modalperiod").val(itemPeriod); $(".modalcurrency, .modalperiod").trigger("refresh"); $("#overlay, #add-modal").addClass("modal-show"); setTimeout(function() { $('.modalvalue').putCursorAtEnd() }, 50); saveOldItem = function() { if (checkModalFields($(".modalvalue").val(), $(".modaltitle").val())) { // If inputs are proper filled, save changes whichColumn[itemId].title = $(".modaltitle").val(); whichColumn[itemId].amount = $(".modalvalue").autoNumeric("get"); whichColumn[itemId].icon = $(".initicons").data("iconselected"); whichColumn[itemId].currency = $(".modalcurrency").val(); whichColumn[itemId].period = $(".modalperiod").val(); editOldDiv(incomesExpenses, itemId, $(".modaltitle").val(), $(".initicons").data("iconselected"), $(".modalcurrency").val(), $(".modalperiod").val(), $(".modalvalue").autoNumeric("get")); turnOffModal(); } checkSlidersLength(); itemsPosition("expense"); itemsPosition("income"); setTimeout(function() { runConvert() }, 230); }; deleteItem = function() { turnOffModal(); delete whichColumn[itemId]; itemDiv.css({"height": "0px", "background-color": "#ffe3e3"}); setTimeout(function() { $("#" + incomesExpenses + "-" + itemId).remove(); checkSlidersLength() }, 300); // Way to proper move items list var slider = $("#" + incomesExpenses + "slider"); if (slider.position().top > -71 && slider.children().length > 4) { setTimeout(function() { Up(incomesExpenses) }, 300); } itemsPosition("expense"); itemsPosition("income"); setTimeout(function() { runConvert() }, 230); // Save changes on server jsonDataSave(); } } function editOldDiv(whichColumn, itemId, itemTitle, itemIcon, itemCurrency, itemPeriod, itemValue) { var value = itemValue.replace(/(\d)(?=(\d\d\d)+([^\d]|$))/g, '$1
'); $("#" + whichColumn + "-" + itemId).replaceWith('
' + itemTitle + '

' + value + ' ' + checkCurrency(itemCurrency) + checkPeriod(itemPeriod) + '

'); $("#" + whichColumn + "-" + itemId).addClass("newitemadded").data({"id": itemId, "icon": itemIcon, "amount": itemValue, "title": itemTitle, "currency": itemCurrency ,"period": itemPeriod}).children("div").addClass(itemIcon); setTimeout(function() { $("#" + whichColumn + "-" + itemId).removeClass("newitemadded") }, 4100); } // prepare calculations for 4 page function runConvert() { expensesSumMonth = 0; incomesSumMonth = 0; $("#circle-select-1, #circle-select-2, #circle-select-3").empty(); getConverted(incomes); getConverted(expenses); } // EVENT HANDLERS // MODAL: Call add items modal window $("#noincomes, #noexpenses, .zoomplus, .plusitemborder").click(function() { $(".mainmodaltitle").empty(); if ($(this).hasClass("incomebutton")) { $(".initicons").data({"iconselected": "wallet", "add-edit": "add", "incomes-expenses": "incomes"}); $("#chooseicon").removeClass().addClass("wallet"); $(".mainmodaltitle").append("Add income"); } else { $(".initicons").data({"iconselected": "cart", "add-edit": "add", "incomes-expenses": "expenses"}); $("#chooseicon").removeClass().addClass("cart"); $(".mainmodaltitle").append("Add expense"); } $(".modalvalue, .modaltitle").show(); $("#overlay, #add-modal").addClass("modal-show"); setTimeout(function() { $('.modalvalue').putCursorAtEnd() }, 50); }); // MODAL: Save button handlers $(".modal-save").click(addOrSaveItems); $(".modaltitle, .modalvalue").keyup(function(e) { if(e.which == 13) { addOrSaveItems(); this.blur() } }); function addOrSaveItems () { if($(".initicons").data("add-edit") == "add") { addNewItem(); } else { saveOldItem(); } jsonDataSave(); } // MODAL: Delete button handler $(".modal-delete").click(function() { deleteItem(); }); // MODAL: Set choosen icon $(".imgbox").click(function() { $(".modaltable").addClass("modalreverse"); setTimeout( function() { $(".modalincomessurface, .modalexpensessurface").fadeOut(150); } , 160); var icon = $(this).children("div").attr('class').split(' ')[1]; $(".initicons").data("iconselected", icon); $("#chooseicon").removeClass().addClass(icon); }); // MODAL: Check that fields are not empty function checkModalFields (itemValue, itemTitle) { if (itemValue == 0) { // Check value input $(".modalvalue").addClass("modalvalueerror"); setTimeout(function() { $(".modalvalue").removeClass("modalvalueerror") }, 500); return false; } else if (itemTitle.length == 0) { // Check title input $(".modaltitle").addClass("modaltitleerror"); setTimeout(function() { $(".modaltitle").removeClass("modaltitleerror") }, 500); return false; } else { return true; } } // MODAL: Start icons table $(".initicons").click(function() { $(".modaltable").removeClass("modalreverse"); if ($(".initicons").data("incomes-expenses") == "incomes") { $(".modalincomessurface").fadeIn(100); } else { $(".modalexpensessurface").fadeIn(100); } }); // MODAL: Turn off modal/notes window and reset all forms $("#overlay, .modal-close").click(turnOffModal); function turnOffModal() { $("#overlay, #add-modal, #add-notes").removeClass("modal-show"); $(".modalincomessurface, .modalexpensessurface").fadeOut(150); setTimeout(function() { $(".modalvalue").val('0').hide(); $(".modaltitle").val('').hide(); $(".modal-delete").hide(); $("textarea#notes").val(user.notes).hide(); }, 200); } // MODAL: Change font size according to input value length $(".modalvalue").bind("keyup keydown keypress select focus click", modalFontSize); function modalFontSize() { var length=$(".modalvalue").val().length; if (length > 10) { $(".modalvalue").css({"font-size": "60px"}); } else if (length > 9) { $(".modalvalue").css({"font-size": "66px"}); } else if (length > 8 ) { $(".modalvalue").css({"font-size": "76px"}); } else { $(".modalvalue").css({"font-size": "86px"}); } } // MODAL: Set zero when input is empty $(".modalvalue").bind("blur", function() { if (this.value.length == 0) { $(".modalvalue").val("0"); } }); // COLUMNS: UP and DOWN buttons $("#expenseup").click(function() { Up("expense"); }); $("#expensedown").click(function() { Down("expense"); }); $("#incomeup").click(function() { Up("income"); }); $("#incomedown").click(function() { Down("income"); }); // COLUMNS: Touch screen swipe $("#expensewrapper").swipe( { swipe:function(event, direction, distance, duration, fingerCount) { if (direction == "up") { Up("expense"); } if (direction == "down") { Down("expense"); } }, threshold:0 }); $("#incomewrapper").swipe( { swipe:function(event, direction, distance, duration, fingerCount) { if (direction == "up") { Up("income"); } if (direction == "down") { Down("income"); } }, threshold:0 }); // COLUMNS: Mouse wheel $('#expensewrapper').bind('DOMMouseScroll mousewheel', function (e){ if(e.originalEvent.wheelDelta > 70) { Down("expense"); } if(e.originalEvent.wheelDelta < -70) { Up("expense"); } if(e.originalEvent.detail < 0) { Down("expense"); } if(e.originalEvent.detail > 0) { Up("expense"); } return false; }); $('#incomewrapper').bind('DOMMouseScroll mousewheel', function (e){ if(e.originalEvent.wheelDelta > 70) { Down("income"); } if(e.originalEvent.wheelDelta < -70) { Up("income"); } if(e.originalEvent.detail < 0) { Down("income"); } if(e.originalEvent.detail > 0) { Up("income"); } return false; }); // SAVINGS: Click on toggles $("#deposit").click(function() { if ($("#deposit").prop('checked') == false) { $("#capitalization").prop('disabled', true); $("#percentvalue").prop('disabled', true); $("#capitalization").prop('checked', false); } else { $("#capitalization").prop('disabled', false); $("#percentvalue").prop('disabled', false); } }); // SAVINGS: Moving ruble sign according to input value length $("#savingsvalue").bind("keyup keydown keypress select click", function() {savings.freeMoney = $("#savingsvalue").autoNumeric("get"); moveRuble();}).keyup(function(e) { if (e.which == 13) { this.blur() } }); function moveRuble() { var length=$("#savingsvalue").val().length; length = (length<2)? 1: length; $("#savingsvalue").attr('size', length); if (length > 9) { $("#savingsvalue").css({"font-size": "31px"}); $("#rublesign").css({"left": (length*15 + 17) + "px", "top": "115px"}); } else if (length > 7 ) { $("#savingsvalue").css({"font-size": "38px"}); $("#rublesign").css({"left": (length*18 + 18) + "px", "top": "120px"}); } else { $("#savingsvalue").css({"font-size": "44px"}); $("#rublesign").css({"left": (length*22 + 20) + "px", "top": "125px"}); } } //SAVINGS: change currency on sign click $("#rublesign").on("click", function() { switch (user.checkedCurr) { case "RUB": user.checkedCurr = "EUR"; $("#rublesign").css({"background-position": "-386px 0"}); break; case "EUR": user.checkedCurr = "USD"; $("#rublesign").css({"background-position": "-354px 0"}); break; case "USD": user.checkedCurr = "RUB"; $("#rublesign").css({"background-position": "-150px 0"}); break; } changeCurrency(); $("#savingsvalue").autoNumeric('set', Math.round (savings.freeMoney) ); moveRuble(); // Update savings slider $('#savings-slider').noUiSlider({ start: (incomesSumMonth-expensesSumMonth) * $("#savings-slider").data("checkedPercent"), step: (incomesSumMonth-expensesSumMonth) / 20, range: { 'min': [ 0 ], 'max': [ incomesSumMonth-expensesSumMonth ] } }, true); runConvert(); }); // SAVINGS: Set zero when input is empty $("#savingsvalue").bind("blur", function() { if (this.value.length == 0) { $("#savingsvalue").val("0"); } }); $("#percentvalue").keyup(function(e) { if(e.which == 13) { this.blur() } }).bind("blur", function() { if (this.value == " %") { $("#percentvalue").val("0 %"); } }); // NOTES: call window $("#bubble").click(function() { $("#overlay, #add-notes").addClass("modal-show"); $(".notes-input").show(); }); // NOTES: save button handler $(".notes-save").click(function() { user.notes = $("textarea#notes").val(); turnOffModal(); jsonDataSave(); }); // Bubble animation $("#bubble").bind("hover", function() { $("#indicator").toggleClass("bubble-animation"); }); // ScrollTop $("input, textarea").bind("blur", function() { $('body').animate({ scrollTop: '0' }, 50) }); // PAGE CHANGING // Cancel swipe on other fields $("body").swipe( { swipe:function(event, direction, distance, duration, fingerCount) { $('body').animate({ scrollTop: '0' }, 50) }, threshold:0 }); // Go back to Greeting Page $("#logoclickplace").click(function() { $("#logo_settings, #overlay, .modal-content, #settingspage, #expenseslider, #bubble").fadeOut(300); setTimeout(greetingPageAgain, 250); }); // All-PAGE SWIPE BETWEEN 3-4 PAGES $("#swipefield, #savings, #savebutton, #settings_hat").bind('DOMMouseScroll mousewheel', function (e){ if(e.originalEvent.wheelDelta < -50) { launchStatistic(); } return false; }); $(".bottompage").bind('DOMMouseScroll mousewheel', function (e){ if(e.originalEvent.wheelDelta > 100) { fadeStatistic(); } return false; }); $("#swipefield, #savings, #savebutton, #settings_hat").swipe( { swipe:function(event, direction, distance, duration, fingerCount) { if (direction == "up" || direction == "left") { if (global.mobileClient) launchStatistic(); } }, threshold:0 }); $(".bottompage").swipe( { swipe:function(event, direction, distance, duration, fingerCount) { if (direction == "down" || direction == "right") { if (global.mobileClient) fadeStatistic(); } }, threshold:0 }); $("#savebutton").on("click", function() { launchStatistic(); }); $("#logo_statistic").on("click", function() { fadeStatistic(); }); // Launch 4 page function launchStatistic() { if ($("#incomeslider").children().length > 0 && $("#expenseslider").children().length > 0) { $(".bottompage").css({"display": "block"}); // Fill objects form Savings fields savings.percent = $("#percentvalue").autoNumeric("get"); savings.deposit = $("#deposit").prop("checked"); savings.capitalization = $("#capitalization").prop("checked"); // Launch 4 page $("#lastlogoflipper").addClass("flippedcard"); initStatisticPage(); setTimeout(function() { initSavingsCircles(user.checkedPercent, 0.2, savings.freeMoney, savings.freeMoney, 700) }, 1600); setTimeout(function() { $(".toppage, .bottompage").addClass("sectionDown"); }, 400); // Update savings slider $('#savings-slider').noUiSlider({ start: (incomesSumMonth-expensesSumMonth) * $("#savings-slider").data("checkedPercent"), step: (incomesSumMonth-expensesSumMonth) / 20, range: { 'min': [ 0 ], 'max': [Math.abs(incomesSumMonth-expensesSumMonth)] } }, true); } else { alert("Please, add at least one item for each column") } jsonDataSave(); } function jsonDataSave() { if (global.savePermit) { $.ajax({ url: 'accounts/current', datatype: 'json', type: "put", contentType: "application/json", headers: {'Authorization': 'Bearer ' + getOauthTokenFromStorage()}, data: JSON.stringify({ note: user.notes, incomes: $.map(incomes, function(value) {return [value]}), expenses: $.map(expenses, function(value) {return [value]}), saving: { amount: Math.ceil(savings.freeMoney), capitalization: savings.capitalization, deposit: savings.deposit, currency: user.checkedCurr, interest: savings.percent } }), success: function () { $("#leftborder, #rightborder, #centerborder").addClass("saveaction"); setTimeout(function() { $("#leftborder, #rightborder, #centerborder").removeClass("saveaction"); }, 400); }, error: function () { alert("An error during data saving. Please, try again later"); } }); } } function fadeStatistic() { switch (user.checkedCurr) { case "RUB": $("#rublesign").css({"background-position": "-150px 0"}); break; case "EUR": $("#rublesign").css({"background-position": "-386px 0"}); break; case "USD": $("#rublesign").css({"background-position": "-354px 0"}); break; } $("#savingsvalue").autoNumeric('set', savings.freeMoney); moveRuble(); $(".toppage, .bottompage").removeClass("sectionDown"); setTimeout(function() { $("#lastlogoflipper").removeClass("flippedcard"); }, 220); setTimeout(function() { drawChartLine(91); $(".bottompage").css({"display": "none"}); }, 500); } ================================================ FILE: gateway/src/test/java/com/piggymetrics/gateway/GatewayApplicationTests.java ================================================ package com.piggymetrics.gateway; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class GatewayApplicationTests { @Test public void contextLoads() { } @Test public void fire() { } } ================================================ FILE: gateway/src/test/resources/bootstrap.yml ================================================ eureka: client: enabled: false ================================================ FILE: mongodb/Dockerfile ================================================ FROM mongo:3 MAINTAINER Alexander Lukyanchikov ADD init.sh /init.sh ADD ./dump / RUN \ chmod +x /init.sh && \ apt-get update && apt-get dist-upgrade -y --force-yes && apt-get install dos2unix && \ apt-get install psmisc -y -q && \ apt-get autoremove -y && apt-get clean && \ rm -rf /var/cache/* && rm -rf /var/lib/apt/lists/* && \ dos2unix -n /init.sh /initx.sh && chmod +x /initx.sh ENTRYPOINT ["/initx.sh"] ================================================ FILE: mongodb/dump/account-service-dump.js ================================================ /** * Creates pre-filled demo account */ print('dump start'); db.accounts.update( { "_id": "demo" }, { "_id": "demo", "lastSeen": new Date(), "note": "demo note", "expenses": [ { "amount": 1300, "currency": "USD", "icon": "home", "period": "MONTH", "title": "Rent" }, { "amount": 120, "currency": "USD", "icon": "utilities", "period": "MONTH", "title": "Utilities" }, { "amount": 20, "currency": "USD", "icon": "meal", "period": "DAY", "title": "Meal" }, { "amount": 240, "currency": "USD", "icon": "gas", "period": "MONTH", "title": "Gas" }, { "amount": 3500, "currency": "EUR", "icon": "island", "period": "YEAR", "title": "Vacation" }, { "amount": 30, "currency": "EUR", "icon": "phone", "period": "MONTH", "title": "Phone" }, { "amount": 700, "currency": "USD", "icon": "sport", "period": "YEAR", "title": "Gym" } ], "incomes": [ { "amount": 42000, "currency": "USD", "icon": "wallet", "period": "YEAR", "title": "Salary" }, { "amount": 500, "currency": "USD", "icon": "edu", "period": "MONTH", "title": "Scholarship" } ], "saving": { "amount": 5900, "capitalization": false, "currency": "USD", "deposit": true, "interest": 3.32 } }, { upsert: true } ); print('dump complete'); ================================================ FILE: mongodb/init.sh ================================================ #!/bin/bash if test -z "$MONGODB_PASSWORD"; then echo "MONGODB_PASSWORD not defined" exit 1 fi auth="-u user -p $MONGODB_PASSWORD" # MONGODB USER CREATION ( echo "setup mongodb auth" create_user="if (!db.getUser('user')) { db.createUser({ user: 'user', pwd: '$MONGODB_PASSWORD', roles: [ {role:'readWrite', db:'piggymetrics'} ]}) }" until mongo piggymetrics --eval "$create_user" || mongo piggymetrics $auth --eval "$create_user"; do sleep 5; done killall mongod sleep 1 killall -9 mongod ) & # INIT DUMP EXECUTION ( if test -n "$INIT_DUMP"; then echo "execute dump file" until mongo piggymetrics $auth $INIT_DUMP; do sleep 5; done fi ) & echo "start mongodb without auth" chown -R mongodb /data/db gosu mongodb mongod "$@" echo "restarting with auth on" sleep 5 exec gosu mongodb /usr/local/bin/docker-entrypoint.sh --auth "$@" ================================================ FILE: monitoring/Dockerfile ================================================ FROM java:8-jre MAINTAINER Alexander Lukyanchikov ADD ./target/monitoring.jar /app/ CMD ["java", "-Xmx200m", "-jar", "/app/monitoring.jar"] EXPOSE 8080 ================================================ FILE: monitoring/pom.xml ================================================ 4.0.0 monitoring 0.0.1-SNAPSHOT jar monitoring com.piggymetrics piggymetrics 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-config org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ${project.name} ================================================ FILE: monitoring/src/main/java/com/piggymetrics/monitoring/MonitoringApplication.java ================================================ package com.piggymetrics.monitoring; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; @SpringBootApplication @EnableHystrixDashboard public class MonitoringApplication { public static void main(String[] args) { SpringApplication.run(MonitoringApplication.class, args); } } ================================================ FILE: monitoring/src/main/resources/bootstrap.yml ================================================ spring: application: name: monitoring cloud: config: uri: http://config:8888 fail-fast: true password: ${CONFIG_SERVICE_PASSWORD} username: user ================================================ FILE: monitoring/src/test/java/com/piggymetrics/monitoring/MonitoringApplicationTests.java ================================================ package com.piggymetrics.monitoring; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class MonitoringApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: monitoring/src/test/resources/bootstrap.yml ================================================ eureka: client: enabled: false ================================================ FILE: notification-service/Dockerfile ================================================ FROM java:8-jre MAINTAINER Alexander Lukyanchikov ADD ./target/notification-service.jar /app/ CMD ["java", "-Xmx200m", "-jar", "/app/notification-service.jar"] EXPOSE 8000 ================================================ FILE: notification-service/pom.xml ================================================ 4.0.0 notification-service 1.0.0-SNAPSHOT jar notification-service com.piggymetrics piggymetrics 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.boot spring-boot-starter-security org.springframework.cloud spring-cloud-starter-config org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-sleuth org.springframework.boot spring-boot-starter-data-mongodb org.springframework.boot spring-boot-starter-actuator org.springframework.cloud spring-cloud-starter-bus-amqp org.springframework.cloud spring-cloud-netflix-hystrix-stream org.springframework.boot spring-boot-starter-mail de.flapdoodle.embed de.flapdoodle.embed.mongo 1.50.3 test com.jayway.jsonpath json-path 2.2.0 test org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin notification-service org.jacoco jacoco-maven-plugin 0.7.6.201602180812 prepare-agent report test report ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/NotificationServiceApplication.java ================================================ package com.piggymetrics.notification; import com.piggymetrics.notification.repository.converter.FrequencyReaderConverter; import com.piggymetrics.notification.repository.converter.FrequencyWriterConverter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.core.convert.CustomConversions; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; import java.util.Arrays; @SpringBootApplication @EnableDiscoveryClient @EnableOAuth2Client @EnableFeignClients @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableScheduling public class NotificationServiceApplication { public static void main(String[] args) { SpringApplication.run(NotificationServiceApplication.class, args); } @Configuration static class CustomConversionsConfig { @Bean public CustomConversions customConversions() { return new CustomConversions(Arrays.asList(new FrequencyReaderConverter(), new FrequencyWriterConverter())); } } } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/client/AccountServiceClient.java ================================================ package com.piggymetrics.notification.client; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @FeignClient(name = "account-service") public interface AccountServiceClient { @RequestMapping(method = RequestMethod.GET, value = "/accounts/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) String getAccount(@PathVariable("accountName") String accountName); } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/config/ResourceServerConfig.java ================================================ package com.piggymetrics.notification.config; import feign.RequestInterceptor; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; /** * @author cdov */ @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Bean @ConfigurationProperties(prefix = "security.oauth2.client") public ClientCredentialsResourceDetails clientCredentialsResourceDetails() { return new ClientCredentialsResourceDetails(); } @Bean public RequestInterceptor oauth2FeignRequestInterceptor(){ return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails()); } @Bean public OAuth2RestTemplate clientCredentialsRestTemplate() { return new OAuth2RestTemplate(clientCredentialsResourceDetails()); } } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/controller/RecipientController.java ================================================ package com.piggymetrics.notification.controller; import com.piggymetrics.notification.domain.Recipient; import com.piggymetrics.notification.service.RecipientService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; import java.security.Principal; @RestController @RequestMapping("/recipients") public class RecipientController { @Autowired private RecipientService recipientService; @RequestMapping(path = "/current", method = RequestMethod.GET) public Object getCurrentNotificationsSettings(Principal principal) { return recipientService.findByAccountName(principal.getName()); } @RequestMapping(path = "/current", method = RequestMethod.PUT) public Object saveCurrentNotificationsSettings(Principal principal, @Valid @RequestBody Recipient recipient) { return recipientService.save(principal.getName(), recipient); } } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/domain/Frequency.java ================================================ package com.piggymetrics.notification.domain; import java.util.stream.Stream; public enum Frequency { WEEKLY(7), MONTHLY(30), QUARTERLY(90); private int days; Frequency(int days) { this.days = days; } public int getDays() { return days; } public static Frequency withDays(int days) { return Stream.of(Frequency.values()) .filter(f -> f.getDays() == days) .findFirst() .orElseThrow(IllegalArgumentException::new); } } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/domain/NotificationSettings.java ================================================ package com.piggymetrics.notification.domain; import javax.validation.constraints.NotNull; import java.util.Date; public class NotificationSettings { @NotNull private Boolean active; @NotNull private Frequency frequency; private Date lastNotified; public Boolean getActive() { return active; } public void setActive(Boolean active) { this.active = active; } public Frequency getFrequency() { return frequency; } public void setFrequency(Frequency frequency) { this.frequency = frequency; } public Date getLastNotified() { return lastNotified; } public void setLastNotified(Date lastNotified) { this.lastNotified = lastNotified; } } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/domain/NotificationType.java ================================================ package com.piggymetrics.notification.domain; public enum NotificationType { BACKUP("backup.email.subject", "backup.email.text", "backup.email.attachment"), REMIND("remind.email.subject", "remind.email.text", null); private String subject; private String text; private String attachment; NotificationType(String subject, String text, String attachment) { this.subject = subject; this.text = text; this.attachment = attachment; } public String getSubject() { return subject; } public String getText() { return text; } public String getAttachment() { return attachment; } } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/domain/Recipient.java ================================================ package com.piggymetrics.notification.domain; import org.hibernate.validator.constraints.Email; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.util.Map; @Document(collection = "recipients") public class Recipient { @Id private String accountName; @NotNull @Email private String email; @Valid private Map scheduledNotifications; public String getAccountName() { return accountName; } public void setAccountName(String accountName) { this.accountName = accountName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Map getScheduledNotifications() { return scheduledNotifications; } public void setScheduledNotifications(Map scheduledNotifications) { this.scheduledNotifications = scheduledNotifications; } @Override public String toString() { return "Recipient{" + "accountName='" + accountName + '\'' + ", email='" + email + '\'' + '}'; } } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/repository/RecipientRepository.java ================================================ package com.piggymetrics.notification.repository; import com.piggymetrics.notification.domain.Recipient; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface RecipientRepository extends CrudRepository { Recipient findByAccountName(String name); @Query("{ $and: [ {'scheduledNotifications.BACKUP.active': true }, { $where: 'this.scheduledNotifications.BACKUP.lastNotified < " + "new Date(new Date().setDate(new Date().getDate() - this.scheduledNotifications.BACKUP.frequency ))' }] }") List findReadyForBackup(); @Query("{ $and: [ {'scheduledNotifications.REMIND.active': true }, { $where: 'this.scheduledNotifications.REMIND.lastNotified < " + "new Date(new Date().setDate(new Date().getDate() - this.scheduledNotifications.REMIND.frequency ))' }] }") List findReadyForRemind(); } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/repository/converter/FrequencyReaderConverter.java ================================================ package com.piggymetrics.notification.repository.converter; import com.piggymetrics.notification.domain.Frequency; import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; @Component public class FrequencyReaderConverter implements Converter { @Override public Frequency convert(Integer days) { return Frequency.withDays(days); } } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/repository/converter/FrequencyWriterConverter.java ================================================ package com.piggymetrics.notification.repository.converter; import com.piggymetrics.notification.domain.Frequency; import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; @Component public class FrequencyWriterConverter implements Converter { @Override public Integer convert(Frequency frequency) { return frequency.getDays(); } } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/service/EmailService.java ================================================ package com.piggymetrics.notification.service; import com.piggymetrics.notification.domain.NotificationType; import com.piggymetrics.notification.domain.Recipient; import javax.mail.MessagingException; import java.io.IOException; public interface EmailService { void send(NotificationType type, Recipient recipient, String attachment) throws MessagingException, IOException; } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/service/EmailServiceImpl.java ================================================ package com.piggymetrics.notification.service; import com.piggymetrics.notification.domain.NotificationType; import com.piggymetrics.notification.domain.Recipient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.core.env.Environment; import org.springframework.core.io.ByteArrayResource; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.IOException; import java.text.MessageFormat; @Service @RefreshScope public class EmailServiceImpl implements EmailService { private final Logger log = LoggerFactory.getLogger(getClass()); @Autowired private JavaMailSender mailSender; @Autowired private Environment env; @Override public void send(NotificationType type, Recipient recipient, String attachment) throws MessagingException, IOException { final String subject = env.getProperty(type.getSubject()); final String text = MessageFormat.format(env.getProperty(type.getText()), recipient.getAccountName()); MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true); helper.setTo(recipient.getEmail()); helper.setSubject(subject); helper.setText(text); if (StringUtils.hasLength(attachment)) { helper.addAttachment(env.getProperty(type.getAttachment()), new ByteArrayResource(attachment.getBytes())); } mailSender.send(message); log.info("{} email notification has been send to {}", type, recipient.getEmail()); } } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/service/NotificationService.java ================================================ package com.piggymetrics.notification.service; public interface NotificationService { void sendBackupNotifications(); void sendRemindNotifications(); } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/service/NotificationServiceImpl.java ================================================ package com.piggymetrics.notification.service; import com.piggymetrics.notification.client.AccountServiceClient; import com.piggymetrics.notification.domain.NotificationType; import com.piggymetrics.notification.domain.Recipient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.CompletableFuture; @Service public class NotificationServiceImpl implements NotificationService { private final Logger log = LoggerFactory.getLogger(getClass()); @Autowired private AccountServiceClient client; @Autowired private RecipientService recipientService; @Autowired private EmailService emailService; @Override @Scheduled(cron = "${backup.cron}") public void sendBackupNotifications() { final NotificationType type = NotificationType.BACKUP; List recipients = recipientService.findReadyToNotify(type); log.info("found {} recipients for backup notification", recipients.size()); recipients.forEach(recipient -> CompletableFuture.runAsync(() -> { try { String attachment = client.getAccount(recipient.getAccountName()); emailService.send(type, recipient, attachment); recipientService.markNotified(type, recipient); } catch (Throwable t) { log.error("an error during backup notification for {}", recipient, t); } })); } @Override @Scheduled(cron = "${remind.cron}") public void sendRemindNotifications() { final NotificationType type = NotificationType.REMIND; List recipients = recipientService.findReadyToNotify(type); log.info("found {} recipients for remind notification", recipients.size()); recipients.forEach(recipient -> CompletableFuture.runAsync(() -> { try { emailService.send(type, recipient, null); recipientService.markNotified(type, recipient); } catch (Throwable t) { log.error("an error during remind notification for {}", recipient, t); } })); } } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/service/RecipientService.java ================================================ package com.piggymetrics.notification.service; import com.piggymetrics.notification.domain.NotificationType; import com.piggymetrics.notification.domain.Recipient; import java.util.List; public interface RecipientService { /** * Finds recipient by account name * * @param accountName * @return recipient */ Recipient findByAccountName(String accountName); /** * Finds recipients, which are ready to be notified * at the moment * * @param type * @return recipients to notify */ List findReadyToNotify(NotificationType type); /** * Creates or updates recipient settings * * @param accountName * @param recipient * @return updated recipient */ Recipient save(String accountName, Recipient recipient); /** * Updates {@link NotificationType} {@code lastNotified} property with current date * for given recipient. * * @param type * @param recipient */ void markNotified(NotificationType type, Recipient recipient); } ================================================ FILE: notification-service/src/main/java/com/piggymetrics/notification/service/RecipientServiceImpl.java ================================================ package com.piggymetrics.notification.service; import com.piggymetrics.notification.domain.NotificationType; import com.piggymetrics.notification.domain.Recipient; import com.piggymetrics.notification.repository.RecipientRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import java.util.Date; import java.util.List; @Service public class RecipientServiceImpl implements RecipientService { private final Logger log = LoggerFactory.getLogger(getClass()); @Autowired private RecipientRepository repository; @Override public Recipient findByAccountName(String accountName) { Assert.hasLength(accountName); return repository.findByAccountName(accountName); } /** * {@inheritDoc} */ @Override public Recipient save(String accountName, Recipient recipient) { recipient.setAccountName(accountName); recipient.getScheduledNotifications().values() .forEach(settings -> { if (settings.getLastNotified() == null) { settings.setLastNotified(new Date()); } }); repository.save(recipient); log.info("recipient {} settings has been updated", recipient); return recipient; } /** * {@inheritDoc} */ @Override public List findReadyToNotify(NotificationType type) { switch (type) { case BACKUP: return repository.findReadyForBackup(); case REMIND: return repository.findReadyForRemind(); default: throw new IllegalArgumentException(); } } /** * {@inheritDoc} */ @Override public void markNotified(NotificationType type, Recipient recipient) { recipient.getScheduledNotifications().get(type).setLastNotified(new Date()); repository.save(recipient); } } ================================================ FILE: notification-service/src/main/resources/bootstrap.yml ================================================ spring: application: name: notification-service cloud: config: uri: http://config:8888 fail-fast: true password: ${CONFIG_SERVICE_PASSWORD} username: user ================================================ FILE: notification-service/src/test/java/com/piggymetrics/notification/NotificationServiceApplicationTests.java ================================================ package com.piggymetrics.notification; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class NotificationServiceApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: notification-service/src/test/java/com/piggymetrics/notification/controller/RecipientControllerTest.java ================================================ package com.piggymetrics.notification.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.piggymetrics.notification.domain.Frequency; import com.piggymetrics.notification.domain.NotificationSettings; import com.piggymetrics.notification.domain.NotificationType; import com.piggymetrics.notification.domain.Recipient; import com.piggymetrics.notification.service.RecipientService; import com.sun.security.auth.UserPrincipal; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @SpringBootTest public class RecipientControllerTest { private static final ObjectMapper mapper = new ObjectMapper(); @InjectMocks private RecipientController recipientController; @Mock private RecipientService recipientService; private MockMvc mockMvc; @Before public void setup() { initMocks(this); this.mockMvc = MockMvcBuilders.standaloneSetup(recipientController).build(); } @Test public void shouldSaveCurrentRecipientSettings() throws Exception { Recipient recipient = getStubRecipient(); String json = mapper.writeValueAsString(recipient); mockMvc.perform(put("/recipients/current").principal(new UserPrincipal(recipient.getAccountName())).contentType(MediaType.APPLICATION_JSON).content(json)) .andExpect(status().isOk()); } @Test public void shouldGetCurrentRecipientSettings() throws Exception { Recipient recipient = getStubRecipient(); when(recipientService.findByAccountName(recipient.getAccountName())).thenReturn(recipient); mockMvc.perform(get("/recipients/current").principal(new UserPrincipal(recipient.getAccountName()))) .andExpect(jsonPath("$.accountName").value(recipient.getAccountName())) .andExpect(status().isOk()); } private Recipient getStubRecipient() { NotificationSettings remind = new NotificationSettings(); remind.setActive(true); remind.setFrequency(Frequency.WEEKLY); remind.setLastNotified(null); NotificationSettings backup = new NotificationSettings(); backup.setActive(false); backup.setFrequency(Frequency.MONTHLY); backup.setLastNotified(null); Recipient recipient = new Recipient(); recipient.setAccountName("test"); recipient.setEmail("test@test.com"); recipient.setScheduledNotifications(ImmutableMap.of( NotificationType.BACKUP, backup, NotificationType.REMIND, remind )); return recipient; } } ================================================ FILE: notification-service/src/test/java/com/piggymetrics/notification/repository/RecipientRepositoryTest.java ================================================ package com.piggymetrics.notification.repository; import com.google.common.collect.ImmutableMap; import com.piggymetrics.notification.domain.Frequency; import com.piggymetrics.notification.domain.NotificationSettings; import com.piggymetrics.notification.domain.NotificationType; import com.piggymetrics.notification.domain.Recipient; import org.apache.commons.lang.time.DateUtils; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.Date; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @RunWith(SpringRunner.class) @DataMongoTest public class RecipientRepositoryTest { @Autowired private RecipientRepository repository; @Test public void shouldFindByAccountName() { NotificationSettings remind = new NotificationSettings(); remind.setActive(true); remind.setFrequency(Frequency.WEEKLY); remind.setLastNotified(new Date(0)); NotificationSettings backup = new NotificationSettings(); backup.setActive(false); backup.setFrequency(Frequency.MONTHLY); backup.setLastNotified(new Date()); Recipient recipient = new Recipient(); recipient.setAccountName("test"); recipient.setEmail("test@test.com"); recipient.setScheduledNotifications(ImmutableMap.of( NotificationType.BACKUP, backup, NotificationType.REMIND, remind )); repository.save(recipient); Recipient found = repository.findByAccountName(recipient.getAccountName()); assertEquals(recipient.getAccountName(), found.getAccountName()); assertEquals(recipient.getEmail(), found.getEmail()); assertEquals(recipient.getScheduledNotifications().get(NotificationType.BACKUP).getActive(), found.getScheduledNotifications().get(NotificationType.BACKUP).getActive()); assertEquals(recipient.getScheduledNotifications().get(NotificationType.BACKUP).getFrequency(), found.getScheduledNotifications().get(NotificationType.BACKUP).getFrequency()); assertEquals(recipient.getScheduledNotifications().get(NotificationType.BACKUP).getLastNotified(), found.getScheduledNotifications().get(NotificationType.BACKUP).getLastNotified()); assertEquals(recipient.getScheduledNotifications().get(NotificationType.REMIND).getActive(), found.getScheduledNotifications().get(NotificationType.REMIND).getActive()); assertEquals(recipient.getScheduledNotifications().get(NotificationType.REMIND).getFrequency(), found.getScheduledNotifications().get(NotificationType.REMIND).getFrequency()); assertEquals(recipient.getScheduledNotifications().get(NotificationType.REMIND).getLastNotified(), found.getScheduledNotifications().get(NotificationType.REMIND).getLastNotified()); } @Test public void shouldFindReadyForRemindWhenFrequencyIsWeeklyAndLastNotifiedWas8DaysAgo() { NotificationSettings remind = new NotificationSettings(); remind.setActive(true); remind.setFrequency(Frequency.WEEKLY); remind.setLastNotified(DateUtils.addDays(new Date(), -8)); Recipient recipient = new Recipient(); recipient.setAccountName("test"); recipient.setEmail("test@test.com"); recipient.setScheduledNotifications(ImmutableMap.of( NotificationType.REMIND, remind )); repository.save(recipient); List found = repository.findReadyForRemind(); assertFalse(found.isEmpty()); } @Test public void shouldNotFindReadyForRemindWhenFrequencyIsWeeklyAndLastNotifiedWasYesterday() { NotificationSettings remind = new NotificationSettings(); remind.setActive(true); remind.setFrequency(Frequency.WEEKLY); remind.setLastNotified(DateUtils.addDays(new Date(), -1)); Recipient recipient = new Recipient(); recipient.setAccountName("test"); recipient.setEmail("test@test.com"); recipient.setScheduledNotifications(ImmutableMap.of( NotificationType.REMIND, remind )); repository.save(recipient); List found = repository.findReadyForRemind(); assertTrue(found.isEmpty()); } @Test public void shouldNotFindReadyForRemindWhenNotificationIsNotActive() { NotificationSettings remind = new NotificationSettings(); remind.setActive(false); remind.setFrequency(Frequency.WEEKLY); remind.setLastNotified(DateUtils.addDays(new Date(), -30)); Recipient recipient = new Recipient(); recipient.setAccountName("test"); recipient.setEmail("test@test.com"); recipient.setScheduledNotifications(ImmutableMap.of( NotificationType.REMIND, remind )); repository.save(recipient); List found = repository.findReadyForRemind(); assertTrue(found.isEmpty()); } @Test public void shouldNotFindReadyForBackupWhenFrequencyIsQuaterly() { NotificationSettings remind = new NotificationSettings(); remind.setActive(true); remind.setFrequency(Frequency.QUARTERLY); remind.setLastNotified(DateUtils.addDays(new Date(), -91)); Recipient recipient = new Recipient(); recipient.setAccountName("test"); recipient.setEmail("test@test.com"); recipient.setScheduledNotifications(ImmutableMap.of( NotificationType.BACKUP, remind )); repository.save(recipient); List found = repository.findReadyForBackup(); assertFalse(found.isEmpty()); } } ================================================ FILE: notification-service/src/test/java/com/piggymetrics/notification/service/EmailServiceImplTest.java ================================================ package com.piggymetrics.notification.service; import com.piggymetrics.notification.domain.NotificationType; import com.piggymetrics.notification.domain.Recipient; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.core.env.Environment; import org.springframework.mail.javamail.JavaMailSender; import javax.mail.MessagingException; import javax.mail.Session; import javax.mail.internet.MimeMessage; import java.io.IOException; import java.util.Properties; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; public class EmailServiceImplTest { @InjectMocks private EmailServiceImpl emailService; @Mock private JavaMailSender mailSender; @Mock private Environment env; @Captor private ArgumentCaptor captor; @Before public void setup() { initMocks(this); when(mailSender.createMimeMessage()) .thenReturn(new MimeMessage(Session.getDefaultInstance(new Properties()))); } @Test public void shouldSendBackupEmail() throws MessagingException, IOException { final String subject = "subject"; final String text = "text"; final String attachment = "attachment.json"; Recipient recipient = new Recipient(); recipient.setAccountName("test"); recipient.setEmail("test@test.com"); when(env.getProperty(NotificationType.BACKUP.getSubject())).thenReturn(subject); when(env.getProperty(NotificationType.BACKUP.getText())).thenReturn(text); when(env.getProperty(NotificationType.BACKUP.getAttachment())).thenReturn(attachment); emailService.send(NotificationType.BACKUP, recipient, "{\"name\":\"test\""); verify(mailSender).send(captor.capture()); MimeMessage message = captor.getValue(); assertEquals(subject, message.getSubject()); // TODO check other fields } @Test public void shouldSendRemindEmail() throws MessagingException, IOException { final String subject = "subject"; final String text = "text"; Recipient recipient = new Recipient(); recipient.setAccountName("test"); recipient.setEmail("test@test.com"); when(env.getProperty(NotificationType.REMIND.getSubject())).thenReturn(subject); when(env.getProperty(NotificationType.REMIND.getText())).thenReturn(text); emailService.send(NotificationType.REMIND, recipient, null); verify(mailSender).send(captor.capture()); MimeMessage message = captor.getValue(); assertEquals(subject, message.getSubject()); // TODO check other fields } } ================================================ FILE: notification-service/src/test/java/com/piggymetrics/notification/service/NotificationServiceImplTest.java ================================================ package com.piggymetrics.notification.service; import com.google.common.collect.ImmutableList; import com.piggymetrics.notification.client.AccountServiceClient; import com.piggymetrics.notification.domain.NotificationType; import com.piggymetrics.notification.domain.Recipient; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import javax.mail.MessagingException; import java.io.IOException; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; public class NotificationServiceImplTest { @InjectMocks private NotificationServiceImpl notificationService; @Mock private RecipientService recipientService; @Mock private AccountServiceClient client; @Mock private EmailService emailService; @Before public void setup() { initMocks(this); } @Test public void shouldSendBackupNotificationsEvenWhenErrorsOccursForSomeRecipients() throws IOException, MessagingException, InterruptedException { final String attachment = "json"; Recipient withError = new Recipient(); withError.setAccountName("with-error"); Recipient withNoError = new Recipient(); withNoError.setAccountName("with-no-error"); when(client.getAccount(withError.getAccountName())).thenThrow(new RuntimeException()); when(client.getAccount(withNoError.getAccountName())).thenReturn(attachment); when(recipientService.findReadyToNotify(NotificationType.BACKUP)).thenReturn(ImmutableList.of(withNoError, withError)); notificationService.sendBackupNotifications(); // TODO test concurrent code in a right way verify(emailService, timeout(100)).send(NotificationType.BACKUP, withNoError, attachment); verify(recipientService, timeout(100)).markNotified(NotificationType.BACKUP, withNoError); verify(recipientService, never()).markNotified(NotificationType.BACKUP, withError); } @Test public void shouldSendRemindNotificationsEvenWhenErrorsOccursForSomeRecipients() throws IOException, MessagingException, InterruptedException { final String attachment = "json"; Recipient withError = new Recipient(); withError.setAccountName("with-error"); Recipient withNoError = new Recipient(); withNoError.setAccountName("with-no-error"); when(recipientService.findReadyToNotify(NotificationType.REMIND)).thenReturn(ImmutableList.of(withNoError, withError)); doThrow(new RuntimeException()).when(emailService).send(NotificationType.REMIND, withError, null); notificationService.sendRemindNotifications(); // TODO test concurrent code in a right way verify(emailService, timeout(100)).send(NotificationType.REMIND, withNoError, null); verify(recipientService, timeout(100)).markNotified(NotificationType.REMIND, withNoError); verify(recipientService, never()).markNotified(NotificationType.REMIND, withError); } } ================================================ FILE: notification-service/src/test/java/com/piggymetrics/notification/service/RecipientServiceImplTest.java ================================================ package com.piggymetrics.notification.service; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.piggymetrics.notification.domain.Frequency; import com.piggymetrics.notification.domain.NotificationSettings; import com.piggymetrics.notification.domain.NotificationType; import com.piggymetrics.notification.domain.Recipient; import com.piggymetrics.notification.repository.RecipientRepository; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.util.Date; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; public class RecipientServiceImplTest { @InjectMocks private RecipientServiceImpl recipientService; @Mock private RecipientRepository repository; @Before public void setup() { initMocks(this); } @Test public void shouldFindByAccountName() { Recipient recipient = new Recipient(); recipient.setAccountName("test"); when(repository.findByAccountName(recipient.getAccountName())).thenReturn(recipient); Recipient found = recipientService.findByAccountName(recipient.getAccountName()); assertEquals(recipient, found); } @Test(expected = IllegalArgumentException.class) public void shouldFailToFindRecipientWhenAccountNameIsEmpty() { recipientService.findByAccountName(""); } @Test public void shouldSaveRecipient() { NotificationSettings remind = new NotificationSettings(); remind.setActive(true); remind.setFrequency(Frequency.WEEKLY); remind.setLastNotified(null); NotificationSettings backup = new NotificationSettings(); backup.setActive(false); backup.setFrequency(Frequency.MONTHLY); backup.setLastNotified(new Date()); Recipient recipient = new Recipient(); recipient.setEmail("test@test.com"); recipient.setScheduledNotifications(ImmutableMap.of( NotificationType.BACKUP, backup, NotificationType.REMIND, remind )); Recipient saved = recipientService.save("test", recipient); verify(repository).save(recipient); assertNotNull(saved.getScheduledNotifications().get(NotificationType.REMIND).getLastNotified()); assertEquals("test", saved.getAccountName()); } @Test public void shouldFindReadyToNotifyWhenNotificationTypeIsBackup() { final List recipients = ImmutableList.of(new Recipient()); when(repository.findReadyForBackup()).thenReturn(recipients); List found = recipientService.findReadyToNotify(NotificationType.BACKUP); assertEquals(recipients, found); } @Test public void shouldFindReadyToNotifyWhenNotificationTypeIsRemind() { final List recipients = ImmutableList.of(new Recipient()); when(repository.findReadyForRemind()).thenReturn(recipients); List found = recipientService.findReadyToNotify(NotificationType.REMIND); assertEquals(recipients, found); } @Test public void shouldMarkAsNotified() { NotificationSettings remind = new NotificationSettings(); remind.setActive(true); remind.setFrequency(Frequency.WEEKLY); remind.setLastNotified(null); Recipient recipient = new Recipient(); recipient.setAccountName("test"); recipient.setEmail("test@test.com"); recipient.setScheduledNotifications(ImmutableMap.of( NotificationType.REMIND, remind )); recipientService.markNotified(NotificationType.REMIND, recipient); assertNotNull(recipient.getScheduledNotifications().get(NotificationType.REMIND).getLastNotified()); verify(repository).save(recipient); } } ================================================ FILE: notification-service/src/test/resources/application.yml ================================================ remind: cron: 0 0 0 * * * email: text: "Hey, {0}! We''ve missed you here on PiggyMetrics. It''s time to check your budget statistics.\r\n\r\nCheers,\r\nPiggyMetrics team" subject: PiggyMetrics reminder backup: cron: 0 0 12 * * * email: text: "Howdy, {0}. Your account backup is ready.\r\n\r\nCheers,\r\nPiggyMetrics team" subject: PiggyMetrics account backup attachment: backup.json spring: data: mongodb: database: piggymetrics port: 0 mail: host: smtp.gmail.com port: 465 username: test password: test properties: mail: smtp: auth: true socketFactory: port: 465 class: javax.net.ssl.SSLSocketFactory fallback: false ssl: enable: true ================================================ FILE: notification-service/src/test/resources/bootstrap.yml ================================================ eureka: client: enabled: false ================================================ FILE: pom.xml ================================================ 4.0.0 com.piggymetrics piggymetrics 1.0-SNAPSHOT pom piggymetrics org.springframework.boot spring-boot-starter-parent 2.0.3.RELEASE UTF-8 Finchley.RELEASE 1.8 org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import config monitoring registry gateway auth-service account-service statistics-service notification-service turbine-stream-service ================================================ FILE: registry/Dockerfile ================================================ FROM java:8-jre MAINTAINER Alexander Lukyanchikov ADD ./target/registry.jar /app/ CMD ["java", "-Xmx200m", "-jar", "/app/registry.jar"] EXPOSE 8761 ================================================ FILE: registry/pom.xml ================================================ 4.0.0 registry 0.0.1-SNAPSHOT jar registry com.piggymetrics piggymetrics 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.cloud spring-cloud-starter-config org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ${project.name} ================================================ FILE: registry/src/main/java/com/piggymetrics/registry/RegistryApplication.java ================================================ package com.piggymetrics.registry; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class RegistryApplication { public static void main(String[] args) { SpringApplication.run(RegistryApplication.class, args); } } ================================================ FILE: registry/src/main/resources/bootstrap.yml ================================================ spring: application: name: registry cloud: config: uri: http://config:8888 fail-fast: true password: ${CONFIG_SERVICE_PASSWORD} username: user eureka: instance: prefer-ip-address: true client: registerWithEureka: false fetchRegistry: false server: waitTimeInMsWhenSyncEmpty: 0 ================================================ FILE: statistics-service/Dockerfile ================================================ FROM java:8-jre MAINTAINER Alexander Lukyanchikov ADD ./target/statistics-service.jar /app/ CMD ["java", "-Xmx200m", "-jar", "/app/statistics-service.jar"] EXPOSE 7000 ================================================ FILE: statistics-service/pom.xml ================================================ 4.0.0 statistics-service 1.0-SNAPSHOT jar statistics-service com.piggymetrics piggymetrics 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-security org.springframework.cloud spring-cloud-starter-config org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-sleuth org.springframework.boot spring-boot-starter-data-mongodb org.springframework.boot spring-boot-starter-actuator org.springframework.cloud spring-cloud-starter-bus-amqp org.springframework.cloud spring-cloud-netflix-hystrix-stream com.google.guava guava 19.0 org.springframework.boot spring-boot-starter-test test de.flapdoodle.embed de.flapdoodle.embed.mongo 1.50.3 test com.jayway.jsonpath json-path 2.2.0 test org.springframework.boot spring-boot-maven-plugin statistics-service org.jacoco jacoco-maven-plugin 0.7.6.201602180812 prepare-agent report test report ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/StatisticsApplication.java ================================================ package com.piggymetrics.statistics; import com.piggymetrics.statistics.repository.converter.DataPointIdReaderConverter; import com.piggymetrics.statistics.repository.converter.DataPointIdWriterConverter; import com.piggymetrics.statistics.service.security.CustomUserInfoTokenServices; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.core.convert.CustomConversions; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; import java.util.Arrays; @SpringBootApplication @EnableDiscoveryClient @EnableOAuth2Client @EnableFeignClients @EnableGlobalMethodSecurity(prePostEnabled = true) public class StatisticsApplication { public static void main(String[] args) { SpringApplication.run(StatisticsApplication.class, args); } @Configuration static class CustomConversionsConfig { @Bean public CustomConversions customConversions() { return new CustomConversions(Arrays.asList(new DataPointIdReaderConverter(), new DataPointIdWriterConverter())); } } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/client/ExchangeRatesClient.java ================================================ package com.piggymetrics.statistics.client; import com.piggymetrics.statistics.domain.Currency; import com.piggymetrics.statistics.domain.ExchangeRatesContainer; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(url = "${rates.url}", name = "rates-client", fallback = ExchangeRatesClientFallback.class) public interface ExchangeRatesClient { @RequestMapping(method = RequestMethod.GET, value = "/latest") ExchangeRatesContainer getRates(@RequestParam("base") Currency base); } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/client/ExchangeRatesClientFallback.java ================================================ package com.piggymetrics.statistics.client; import com.piggymetrics.statistics.domain.Currency; import com.piggymetrics.statistics.domain.ExchangeRatesContainer; import org.springframework.stereotype.Component; import java.util.Collections; @Component public class ExchangeRatesClientFallback implements ExchangeRatesClient { @Override public ExchangeRatesContainer getRates(Currency base) { ExchangeRatesContainer container = new ExchangeRatesContainer(); container.setBase(Currency.getBase()); container.setRates(Collections.emptyMap()); return container; } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/config/ResourceServerConfig.java ================================================ package com.piggymetrics.statistics.config; import com.piggymetrics.statistics.service.security.CustomUserInfoTokenServices; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; /** * @author cdov */ @EnableResourceServer @Configuration public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Autowired private ResourceServerProperties sso; @Bean public ResourceServerTokenServices tokenServices() { return new CustomUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId()); } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/controller/StatisticsController.java ================================================ package com.piggymetrics.statistics.controller; import com.piggymetrics.statistics.domain.Account; import com.piggymetrics.statistics.domain.timeseries.DataPoint; import com.piggymetrics.statistics.service.StatisticsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.security.Principal; import java.util.List; @RestController public class StatisticsController { @Autowired private StatisticsService statisticsService; @RequestMapping(value = "/current", method = RequestMethod.GET) public List getCurrentAccountStatistics(Principal principal) { return statisticsService.findByAccountName(principal.getName()); } @PreAuthorize("#oauth2.hasScope('server') or #accountName.equals('demo')") @RequestMapping(value = "/{accountName}", method = RequestMethod.GET) public List getStatisticsByAccountName(@PathVariable String accountName) { return statisticsService.findByAccountName(accountName); } @PreAuthorize("#oauth2.hasScope('server')") @RequestMapping(value = "/{accountName}", method = RequestMethod.PUT) public void saveAccountStatistics(@PathVariable String accountName, @Valid @RequestBody Account account) { statisticsService.save(accountName, account); } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/domain/Account.java ================================================ package com.piggymetrics.statistics.domain; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.springframework.data.mongodb.core.mapping.Document; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.util.List; @Document(collection = "accounts") @JsonIgnoreProperties(ignoreUnknown = true) public class Account { @Valid @NotNull private List incomes; @Valid @NotNull private List expenses; @Valid @NotNull private Saving saving; public List getIncomes() { return incomes; } public void setIncomes(List incomes) { this.incomes = incomes; } public List getExpenses() { return expenses; } public void setExpenses(List expenses) { this.expenses = expenses; } public Saving getSaving() { return saving; } public void setSaving(Saving saving) { this.saving = saving; } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/domain/Currency.java ================================================ package com.piggymetrics.statistics.domain; public enum Currency { USD, EUR, RUB; public static Currency getBase() { return USD; } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/domain/ExchangeRatesContainer.java ================================================ package com.piggymetrics.statistics.domain; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.math.BigDecimal; import java.time.LocalDate; import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true, value = {"date"}) public class ExchangeRatesContainer { private LocalDate date = LocalDate.now(); private Currency base; private Map rates; public LocalDate getDate() { return date; } public void setDate(LocalDate date) { this.date = date; } public Currency getBase() { return base; } public void setBase(Currency base) { this.base = base; } public Map getRates() { return rates; } public void setRates(Map rates) { this.rates = rates; } @Override public String toString() { return "RateList{" + "date=" + date + ", base=" + base + ", rates=" + rates + '}'; } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/domain/Item.java ================================================ package com.piggymetrics.statistics.domain; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; import java.math.BigDecimal; public class Item { @NotNull @Length(min = 1, max = 20) private String title; @NotNull private BigDecimal amount; @NotNull private Currency currency; @NotNull private TimePeriod period; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public BigDecimal getAmount() { return amount; } public void setAmount(BigDecimal amount) { this.amount = amount; } public Currency getCurrency() { return currency; } public void setCurrency(Currency currency) { this.currency = currency; } public TimePeriod getPeriod() { return period; } public void setPeriod(TimePeriod period) { this.period = period; } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/domain/Saving.java ================================================ package com.piggymetrics.statistics.domain; import javax.validation.constraints.NotNull; import java.math.BigDecimal; public class Saving { @NotNull private BigDecimal amount; @NotNull private Currency currency; @NotNull private BigDecimal interest; @NotNull private Boolean deposit; @NotNull private Boolean capitalization; public BigDecimal getAmount() { return amount; } public void setAmount(BigDecimal amount) { this.amount = amount; } public Currency getCurrency() { return currency; } public void setCurrency(Currency currency) { this.currency = currency; } public BigDecimal getInterest() { return interest; } public void setInterest(BigDecimal interest) { this.interest = interest; } public Boolean getDeposit() { return deposit; } public void setDeposit(Boolean deposit) { this.deposit = deposit; } public Boolean getCapitalization() { return capitalization; } public void setCapitalization(Boolean capitalization) { this.capitalization = capitalization; } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/domain/TimePeriod.java ================================================ package com.piggymetrics.statistics.domain; import java.math.BigDecimal; public enum TimePeriod { YEAR(365.2425), QUARTER(91.3106), MONTH(30.4368), DAY(1), HOUR(0.0416); private double baseRatio; TimePeriod(double baseRatio) { this.baseRatio = baseRatio; } public BigDecimal getBaseRatio() { return new BigDecimal(baseRatio); } public static TimePeriod getBase() { return DAY; } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/domain/timeseries/DataPoint.java ================================================ package com.piggymetrics.statistics.domain.timeseries; import com.piggymetrics.statistics.domain.Currency; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import java.math.BigDecimal; import java.util.Map; import java.util.Set; /** * Represents daily time series data point containing * current account state */ @Document(collection = "datapoints") public class DataPoint { @Id private DataPointId id; private Set incomes; private Set expenses; private Map statistics; private Map rates; public DataPointId getId() { return id; } public void setId(DataPointId id) { this.id = id; } public Set getIncomes() { return incomes; } public void setIncomes(Set incomes) { this.incomes = incomes; } public Set getExpenses() { return expenses; } public void setExpenses(Set expenses) { this.expenses = expenses; } public Map getStatistics() { return statistics; } public void setStatistics(Map statistics) { this.statistics = statistics; } public Map getRates() { return rates; } public void setRates(Map rates) { this.rates = rates; } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/domain/timeseries/DataPointId.java ================================================ package com.piggymetrics.statistics.domain.timeseries; import java.io.Serializable; import java.util.Date; public class DataPointId implements Serializable { private static final long serialVersionUID = 1L; private String account; private Date date; public DataPointId(String account, Date date) { this.account = account; this.date = date; } public String getAccount() { return account; } public Date getDate() { return date; } @Override public String toString() { return "DataPointId{" + "account='" + account + '\'' + ", date=" + date + '}'; } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/domain/timeseries/ItemMetric.java ================================================ package com.piggymetrics.statistics.domain.timeseries; import com.piggymetrics.statistics.domain.Currency; import com.piggymetrics.statistics.domain.TimePeriod; import java.math.BigDecimal; /** * Represents normalized {@link com.piggymetrics.statistics.domain.Item} object * with {@link Currency#getBase()} currency and {@link TimePeriod#getBase()} time period */ public class ItemMetric { private String title; private BigDecimal amount; public ItemMetric(String title, BigDecimal amount) { this.title = title; this.amount = amount; } public String getTitle() { return title; } public BigDecimal getAmount() { return amount; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ItemMetric that = (ItemMetric) o; return title.equalsIgnoreCase(that.title); } @Override public int hashCode() { return title.hashCode(); } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/domain/timeseries/StatisticMetric.java ================================================ package com.piggymetrics.statistics.domain.timeseries; public enum StatisticMetric { INCOMES_AMOUNT, EXPENSES_AMOUNT, SAVING_AMOUNT } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/repository/DataPointRepository.java ================================================ package com.piggymetrics.statistics.repository; import com.piggymetrics.statistics.domain.timeseries.DataPoint; import com.piggymetrics.statistics.domain.timeseries.DataPointId; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface DataPointRepository extends CrudRepository { List findByIdAccount(String account); } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/repository/converter/DataPointIdReaderConverter.java ================================================ package com.piggymetrics.statistics.repository.converter; import com.mongodb.DBObject; import com.piggymetrics.statistics.domain.timeseries.DataPointId; import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; import java.util.Date; @Component public class DataPointIdReaderConverter implements Converter { @Override public DataPointId convert(DBObject object) { Date date = (Date) object.get("date"); String account = (String) object.get("account"); return new DataPointId(account, date); } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/repository/converter/DataPointIdWriterConverter.java ================================================ package com.piggymetrics.statistics.repository.converter; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; import com.piggymetrics.statistics.domain.timeseries.DataPointId; import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; @Component public class DataPointIdWriterConverter implements Converter { private static final int FIELDS = 2; @Override public DBObject convert(DataPointId id) { DBObject object = new BasicDBObject(FIELDS); object.put("date", id.getDate()); object.put("account", id.getAccount()); return object; } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/service/ExchangeRatesService.java ================================================ package com.piggymetrics.statistics.service; import com.piggymetrics.statistics.domain.Currency; import java.math.BigDecimal; import java.util.Map; public interface ExchangeRatesService { /** * Requests today's foreign exchange rates from a provider * or reuses values from the last request (if they are still relevant) * * @return current date rates */ Map getCurrentRates(); /** * Converts given amount to specified currency * * @param from {@link Currency} * @param to {@link Currency} * @param amount to be converted * @return converted amount */ BigDecimal convert(Currency from, Currency to, BigDecimal amount); } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/service/ExchangeRatesServiceImpl.java ================================================ package com.piggymetrics.statistics.service; import com.google.common.collect.ImmutableMap; import com.piggymetrics.statistics.client.ExchangeRatesClient; import com.piggymetrics.statistics.domain.Currency; import com.piggymetrics.statistics.domain.ExchangeRatesContainer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.util.Map; @Service public class ExchangeRatesServiceImpl implements ExchangeRatesService { private static final Logger log = LoggerFactory.getLogger(ExchangeRatesServiceImpl.class); private ExchangeRatesContainer container; @Autowired private ExchangeRatesClient client; /** * {@inheritDoc} */ @Override public Map getCurrentRates() { if (container == null || !container.getDate().equals(LocalDate.now())) { container = client.getRates(Currency.getBase()); log.info("exchange rates has been updated: {}", container); } return ImmutableMap.of( Currency.EUR, container.getRates().get(Currency.EUR.name()), Currency.RUB, container.getRates().get(Currency.RUB.name()), Currency.USD, BigDecimal.ONE ); } /** * {@inheritDoc} */ @Override public BigDecimal convert(Currency from, Currency to, BigDecimal amount) { Assert.notNull(amount); Map rates = getCurrentRates(); BigDecimal ratio = rates.get(to).divide(rates.get(from), 4, RoundingMode.HALF_UP); return amount.multiply(ratio); } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/service/StatisticsService.java ================================================ package com.piggymetrics.statistics.service; import com.piggymetrics.statistics.domain.Account; import com.piggymetrics.statistics.domain.timeseries.DataPoint; import java.util.List; public interface StatisticsService { /** * Finds account by given name * * @param accountName * @return found account */ List findByAccountName(String accountName); /** * Converts given {@link Account} object to {@link DataPoint} with * a set of significant statistic metrics. * * Compound {@link DataPoint#id} forces to rewrite the object * for each account within a day. * * @param accountName * @param account */ DataPoint save(String accountName, Account account); } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/service/StatisticsServiceImpl.java ================================================ package com.piggymetrics.statistics.service; import com.google.common.collect.ImmutableMap; import com.piggymetrics.statistics.domain.*; import com.piggymetrics.statistics.domain.timeseries.DataPoint; import com.piggymetrics.statistics.domain.timeseries.DataPointId; import com.piggymetrics.statistics.domain.timeseries.ItemMetric; import com.piggymetrics.statistics.domain.timeseries.StatisticMetric; import com.piggymetrics.statistics.repository.DataPointRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @Service public class StatisticsServiceImpl implements StatisticsService { private final Logger log = LoggerFactory.getLogger(getClass()); @Autowired private DataPointRepository repository; @Autowired private ExchangeRatesService ratesService; /** * {@inheritDoc} */ @Override public List findByAccountName(String accountName) { Assert.hasLength(accountName); return repository.findByIdAccount(accountName); } /** * {@inheritDoc} */ @Override public DataPoint save(String accountName, Account account) { Instant instant = LocalDate.now().atStartOfDay() .atZone(ZoneId.systemDefault()).toInstant(); DataPointId pointId = new DataPointId(accountName, Date.from(instant)); Set incomes = account.getIncomes().stream() .map(this::createItemMetric) .collect(Collectors.toSet()); Set expenses = account.getExpenses().stream() .map(this::createItemMetric) .collect(Collectors.toSet()); Map statistics = createStatisticMetrics(incomes, expenses, account.getSaving()); DataPoint dataPoint = new DataPoint(); dataPoint.setId(pointId); dataPoint.setIncomes(incomes); dataPoint.setExpenses(expenses); dataPoint.setStatistics(statistics); dataPoint.setRates(ratesService.getCurrentRates()); log.debug("new datapoint has been created: {}", pointId); return repository.save(dataPoint); } private Map createStatisticMetrics(Set incomes, Set expenses, Saving saving) { BigDecimal savingAmount = ratesService.convert(saving.getCurrency(), Currency.getBase(), saving.getAmount()); BigDecimal expensesAmount = expenses.stream() .map(ItemMetric::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal incomesAmount = incomes.stream() .map(ItemMetric::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); return ImmutableMap.of( StatisticMetric.EXPENSES_AMOUNT, expensesAmount, StatisticMetric.INCOMES_AMOUNT, incomesAmount, StatisticMetric.SAVING_AMOUNT, savingAmount ); } /** * Normalizes given item amount to {@link Currency#getBase()} currency with * {@link TimePeriod#getBase()} time period */ private ItemMetric createItemMetric(Item item) { BigDecimal amount = ratesService .convert(item.getCurrency(), Currency.getBase(), item.getAmount()) .divide(item.getPeriod().getBaseRatio(), 4, RoundingMode.HALF_UP); return new ItemMetric(item.getTitle(), amount); } } ================================================ FILE: statistics-service/src/main/java/com/piggymetrics/statistics/service/security/CustomUserInfoTokenServices.java ================================================ package com.piggymetrics.statistics.service.security; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor; import org.springframework.boot.autoconfigure.security.oauth2.resource.FixedAuthoritiesExtractor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.client.OAuth2RestOperations; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.OAuth2Request; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; import java.util.*; /** * Extended implementation of {@link org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices} * * By default, it designed to return only user details. This class provides {@link #getRequest(Map)} method, which * returns clientId and scope of calling service. This information used in controller's security checks. */ public class CustomUserInfoTokenServices implements ResourceServerTokenServices { protected final Log logger = LogFactory.getLog(getClass()); private static final String[] PRINCIPAL_KEYS = new String[] { "user", "username", "userid", "user_id", "login", "id", "name" }; private final String userInfoEndpointUrl; private final String clientId; private OAuth2RestOperations restTemplate; private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE; private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor(); public CustomUserInfoTokenServices(String userInfoEndpointUrl, String clientId) { this.userInfoEndpointUrl = userInfoEndpointUrl; this.clientId = clientId; } public void setTokenType(String tokenType) { this.tokenType = tokenType; } public void setRestTemplate(OAuth2RestOperations restTemplate) { this.restTemplate = restTemplate; } public void setAuthoritiesExtractor(AuthoritiesExtractor authoritiesExtractor) { this.authoritiesExtractor = authoritiesExtractor; } @Override public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { Map map = getMap(this.userInfoEndpointUrl, accessToken); if (map.containsKey("error")) { this.logger.debug("userinfo returned error: " + map.get("error")); throw new InvalidTokenException(accessToken); } return extractAuthentication(map); } private OAuth2Authentication extractAuthentication(Map map) { Object principal = getPrincipal(map); OAuth2Request request = getRequest(map); List authorities = this.authoritiesExtractor .extractAuthorities(map); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( principal, "N/A", authorities); token.setDetails(map); return new OAuth2Authentication(request, token); } private Object getPrincipal(Map map) { for (String key : PRINCIPAL_KEYS) { if (map.containsKey(key)) { return map.get(key); } } return "unknown"; } @SuppressWarnings({ "unchecked" }) private OAuth2Request getRequest(Map map) { Map request = (Map) map.get("oauth2Request"); String clientId = (String) request.get("clientId"); Set scope = new LinkedHashSet<>(request.containsKey("scope") ? (Collection) request.get("scope") : Collections.emptySet()); return new OAuth2Request(null, clientId, null, true, new HashSet<>(scope), null, null, null, null); } @Override public OAuth2AccessToken readAccessToken(String accessToken) { throw new UnsupportedOperationException("Not supported: read access token"); } @SuppressWarnings({ "unchecked" }) private Map getMap(String path, String accessToken) { this.logger.debug("Getting user info from: " + path); try { OAuth2RestOperations restTemplate = this.restTemplate; if (restTemplate == null) { BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails(); resource.setClientId(this.clientId); restTemplate = new OAuth2RestTemplate(resource); } OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext() .getAccessToken(); if (existingToken == null || !accessToken.equals(existingToken.getValue())) { DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken( accessToken); token.setTokenType(this.tokenType); restTemplate.getOAuth2ClientContext().setAccessToken(token); } return restTemplate.getForEntity(path, Map.class).getBody(); } catch (Exception ex) { this.logger.info("Could not fetch user details: " + ex.getClass() + ", " + ex.getMessage()); return Collections.singletonMap("error", "Could not fetch user details"); } } } ================================================ FILE: statistics-service/src/main/resources/bootstrap.yml ================================================ spring: application: name: statistics-service cloud: config: uri: http://config:8888 fail-fast: true password: ${CONFIG_SERVICE_PASSWORD} username: user ================================================ FILE: statistics-service/src/test/java/com/piggymetrics/statistics/StatisticsServiceApplicationTests.java ================================================ package com.piggymetrics.statistics; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class StatisticsServiceApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: statistics-service/src/test/java/com/piggymetrics/statistics/client/ExchangeRatesClientTest.java ================================================ package com.piggymetrics.statistics.client; import com.piggymetrics.statistics.domain.Currency; import com.piggymetrics.statistics.domain.ExchangeRatesContainer; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.time.LocalDate; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @RunWith(SpringRunner.class) @SpringBootTest public class ExchangeRatesClientTest { @Autowired private ExchangeRatesClient client; @Test public void shouldRetrieveExchangeRates() { ExchangeRatesContainer container = client.getRates(Currency.getBase()); assertEquals(container.getDate(), LocalDate.now()); assertEquals(container.getBase(), Currency.getBase()); assertNotNull(container.getRates()); assertNotNull(container.getRates().get(Currency.USD.name())); assertNotNull(container.getRates().get(Currency.EUR.name())); assertNotNull(container.getRates().get(Currency.RUB.name())); } @Test public void shouldRetrieveExchangeRatesForSpecifiedCurrency() { Currency requestedCurrency = Currency.EUR; ExchangeRatesContainer container = client.getRates(Currency.getBase()); assertEquals(container.getDate(), LocalDate.now()); assertEquals(container.getBase(), Currency.getBase()); assertNotNull(container.getRates()); assertNotNull(container.getRates().get(requestedCurrency.name())); } } ================================================ FILE: statistics-service/src/test/java/com/piggymetrics/statistics/controller/StatisticsControllerTest.java ================================================ package com.piggymetrics.statistics.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.piggymetrics.statistics.domain.Account; import com.piggymetrics.statistics.domain.Currency; import com.piggymetrics.statistics.domain.Item; import com.piggymetrics.statistics.domain.Saving; import com.piggymetrics.statistics.domain.TimePeriod; import com.piggymetrics.statistics.domain.timeseries.DataPoint; import com.piggymetrics.statistics.domain.timeseries.DataPointId; import com.piggymetrics.statistics.service.StatisticsService; import com.sun.security.auth.UserPrincipal; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.math.BigDecimal; import java.util.Date; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static org.mockito.internal.verification.VerificationModeFactory.times; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @SpringBootTest public class StatisticsControllerTest { private static final ObjectMapper mapper = new ObjectMapper(); @InjectMocks private StatisticsController statisticsController; @Mock private StatisticsService statisticsService; private MockMvc mockMvc; @Before public void setup() { initMocks(this); this.mockMvc = MockMvcBuilders.standaloneSetup(statisticsController).build(); } @Test public void shouldGetStatisticsByAccountName() throws Exception { final DataPoint dataPoint = new DataPoint(); dataPoint.setId(new DataPointId("test", new Date())); when(statisticsService.findByAccountName(dataPoint.getId().getAccount())) .thenReturn(ImmutableList.of(dataPoint)); mockMvc.perform(get("/test").principal(new UserPrincipal(dataPoint.getId().getAccount()))) .andExpect(jsonPath("$[0].id.account").value(dataPoint.getId().getAccount())) .andExpect(status().isOk()); } @Test public void shouldGetCurrentAccountStatistics() throws Exception { final DataPoint dataPoint = new DataPoint(); dataPoint.setId(new DataPointId("test", new Date())); when(statisticsService.findByAccountName(dataPoint.getId().getAccount())) .thenReturn(ImmutableList.of(dataPoint)); mockMvc.perform(get("/current").principal(new UserPrincipal(dataPoint.getId().getAccount()))) .andExpect(jsonPath("$[0].id.account").value(dataPoint.getId().getAccount())) .andExpect(status().isOk()); } @Test public void shouldSaveAccountStatistics() throws Exception { Saving saving = new Saving(); saving.setAmount(new BigDecimal(1500)); saving.setCurrency(Currency.USD); saving.setInterest(new BigDecimal("3.32")); saving.setDeposit(true); saving.setCapitalization(false); Item grocery = new Item(); grocery.setTitle("Grocery"); grocery.setAmount(new BigDecimal(10)); grocery.setCurrency(Currency.USD); grocery.setPeriod(TimePeriod.DAY); Item salary = new Item(); salary.setTitle("Salary"); salary.setAmount(new BigDecimal(9100)); salary.setCurrency(Currency.USD); salary.setPeriod(TimePeriod.MONTH); final Account account = new Account(); account.setSaving(saving); account.setExpenses(ImmutableList.of(grocery)); account.setIncomes(ImmutableList.of(salary)); String json = mapper.writeValueAsString(account); mockMvc.perform(put("/test").contentType(MediaType.APPLICATION_JSON).content(json)) .andExpect(status().isOk()); verify(statisticsService, times(1)).save(anyString(), any(Account.class)); } } ================================================ FILE: statistics-service/src/test/java/com/piggymetrics/statistics/repository/DataPointRepositoryTest.java ================================================ package com.piggymetrics.statistics.repository; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import com.piggymetrics.statistics.domain.timeseries.DataPoint; import com.piggymetrics.statistics.domain.timeseries.DataPointId; import com.piggymetrics.statistics.domain.timeseries.ItemMetric; import com.piggymetrics.statistics.domain.timeseries.StatisticMetric; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; import org.springframework.test.context.junit4.SpringRunner; import java.math.BigDecimal; import java.util.Date; import java.util.List; import static org.junit.Assert.assertEquals; @RunWith(SpringRunner.class) @DataMongoTest public class DataPointRepositoryTest { @Autowired private DataPointRepository repository; @Test public void shouldSaveDataPoint() { ItemMetric salary = new ItemMetric("salary", new BigDecimal(20_000)); ItemMetric grocery = new ItemMetric("grocery", new BigDecimal(1_000)); ItemMetric vacation = new ItemMetric("vacation", new BigDecimal(2_000)); DataPointId pointId = new DataPointId("test-account", new Date(0)); DataPoint point = new DataPoint(); point.setId(pointId); point.setIncomes(Sets.newHashSet(salary)); point.setExpenses(Sets.newHashSet(grocery, vacation)); point.setStatistics(ImmutableMap.of( StatisticMetric.SAVING_AMOUNT, new BigDecimal(400_000), StatisticMetric.INCOMES_AMOUNT, new BigDecimal(20_000), StatisticMetric.EXPENSES_AMOUNT, new BigDecimal(3_000) )); repository.save(point); List points = repository.findByIdAccount(pointId.getAccount()); assertEquals(1, points.size()); assertEquals(pointId.getDate(), points.get(0).getId().getDate()); assertEquals(point.getStatistics().size(), points.get(0).getStatistics().size()); assertEquals(point.getIncomes().size(), points.get(0).getIncomes().size()); assertEquals(point.getExpenses().size(), points.get(0).getExpenses().size()); } @Test public void shouldRewriteDataPointWithinADay() { final BigDecimal earlyAmount = new BigDecimal(100); final BigDecimal lateAmount = new BigDecimal(200); DataPointId pointId = new DataPointId("test-account", new Date(0)); DataPoint earlier = new DataPoint(); earlier.setId(pointId); earlier.setStatistics(ImmutableMap.of( StatisticMetric.SAVING_AMOUNT, earlyAmount )); repository.save(earlier); DataPoint later = new DataPoint(); later.setId(pointId); later.setStatistics(ImmutableMap.of( StatisticMetric.SAVING_AMOUNT, lateAmount )); repository.save(later); List points = repository.findByIdAccount(pointId.getAccount()); assertEquals(1, points.size()); assertEquals(lateAmount, points.get(0).getStatistics().get(StatisticMetric.SAVING_AMOUNT)); } } ================================================ FILE: statistics-service/src/test/java/com/piggymetrics/statistics/service/ExchangeRatesServiceImplTest.java ================================================ package com.piggymetrics.statistics.service; import com.google.common.collect.ImmutableMap; import com.piggymetrics.statistics.client.ExchangeRatesClient; import com.piggymetrics.statistics.domain.Currency; import com.piggymetrics.statistics.domain.ExchangeRatesContainer; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.math.BigDecimal; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; public class ExchangeRatesServiceImplTest { @InjectMocks private ExchangeRatesServiceImpl ratesService; @Mock private ExchangeRatesClient client; @Before public void setup() { initMocks(this); } @Test public void shouldReturnCurrentRatesWhenContainerIsEmptySoFar() { ExchangeRatesContainer container = new ExchangeRatesContainer(); container.setRates(ImmutableMap.of( Currency.EUR.name(), new BigDecimal("0.8"), Currency.RUB.name(), new BigDecimal("80") )); when(client.getRates(Currency.getBase())).thenReturn(container); Map result = ratesService.getCurrentRates(); verify(client, times(1)).getRates(Currency.getBase()); assertEquals(container.getRates().get(Currency.EUR.name()), result.get(Currency.EUR)); assertEquals(container.getRates().get(Currency.RUB.name()), result.get(Currency.RUB)); assertEquals(BigDecimal.ONE, result.get(Currency.USD)); } @Test public void shouldNotRequestRatesWhenTodaysContainerAlreadyExists() { ExchangeRatesContainer container = new ExchangeRatesContainer(); container.setRates(ImmutableMap.of( Currency.EUR.name(), new BigDecimal("0.8"), Currency.RUB.name(), new BigDecimal("80") )); when(client.getRates(Currency.getBase())).thenReturn(container); // initialize container ratesService.getCurrentRates(); // use existing container ratesService.getCurrentRates(); verify(client, times(1)).getRates(Currency.getBase()); } @Test public void shouldConvertCurrency() { ExchangeRatesContainer container = new ExchangeRatesContainer(); container.setRates(ImmutableMap.of( Currency.EUR.name(), new BigDecimal("0.8"), Currency.RUB.name(), new BigDecimal("80") )); when(client.getRates(Currency.getBase())).thenReturn(container); final BigDecimal amount = new BigDecimal(100); final BigDecimal expectedConvertionResult = new BigDecimal("1.25"); BigDecimal result = ratesService.convert(Currency.RUB, Currency.USD, amount); assertTrue(expectedConvertionResult.compareTo(result) == 0); } @Test(expected = IllegalArgumentException.class) public void shouldFailToConvertWhenAmountIsNull() { ratesService.convert(Currency.EUR, Currency.RUB, null); } } ================================================ FILE: statistics-service/src/test/java/com/piggymetrics/statistics/service/StatisticsServiceImplTest.java ================================================ package com.piggymetrics.statistics.service; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.piggymetrics.statistics.domain.Account; import com.piggymetrics.statistics.domain.Currency; import com.piggymetrics.statistics.domain.Item; import com.piggymetrics.statistics.domain.Saving; import com.piggymetrics.statistics.domain.TimePeriod; import com.piggymetrics.statistics.domain.timeseries.DataPoint; import com.piggymetrics.statistics.domain.timeseries.ItemMetric; import com.piggymetrics.statistics.domain.timeseries.StatisticMetric; import com.piggymetrics.statistics.repository.DataPointRepository; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.ZoneId; import java.util.Date; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.AdditionalAnswers.returnsFirstArg; import static org.mockito.Mockito.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; public class StatisticsServiceImplTest { @InjectMocks private StatisticsServiceImpl statisticsService; @Mock private ExchangeRatesServiceImpl ratesService; @Mock private DataPointRepository repository; @Before public void setup() { initMocks(this); } @Test public void shouldFindDataPointListByAccountName() { final List list = ImmutableList.of(new DataPoint()); when(repository.findByIdAccount("test")).thenReturn(list); List result = statisticsService.findByAccountName("test"); assertEquals(list, result); } @Test(expected = IllegalArgumentException.class) public void shouldFailToFindDataPointWhenAccountNameIsNull() { statisticsService.findByAccountName(null); } @Test(expected = IllegalArgumentException.class) public void shouldFailToFindDataPointWhenAccountNameIsEmpty() { statisticsService.findByAccountName(""); } @Test public void shouldSaveDataPoint() { /** * Given */ Item salary = new Item(); salary.setTitle("Salary"); salary.setAmount(new BigDecimal(9100)); salary.setCurrency(Currency.USD); salary.setPeriod(TimePeriod.MONTH); Item grocery = new Item(); grocery.setTitle("Grocery"); grocery.setAmount(new BigDecimal(500)); grocery.setCurrency(Currency.RUB); grocery.setPeriod(TimePeriod.DAY); Item vacation = new Item(); vacation.setTitle("Vacation"); vacation.setAmount(new BigDecimal(3400)); vacation.setCurrency(Currency.EUR); vacation.setPeriod(TimePeriod.YEAR); Saving saving = new Saving(); saving.setAmount(new BigDecimal(1000)); saving.setCurrency(Currency.EUR); saving.setInterest(new BigDecimal(3.2)); saving.setDeposit(true); saving.setCapitalization(false); Account account = new Account(); account.setIncomes(ImmutableList.of(salary)); account.setExpenses(ImmutableList.of(grocery, vacation)); account.setSaving(saving); final Map rates = ImmutableMap.of( Currency.EUR, new BigDecimal("0.8"), Currency.RUB, new BigDecimal("80"), Currency.USD, BigDecimal.ONE ); /** * When */ when(ratesService.convert(any(Currency.class),any(Currency.class),any(BigDecimal.class))) .then(i -> ((BigDecimal)i.getArgument(2)) .divide(rates.get(i.getArgument(0)), 4, RoundingMode.HALF_UP)); when(ratesService.getCurrentRates()).thenReturn(rates); when(repository.save(any(DataPoint.class))).then(returnsFirstArg()); DataPoint dataPoint = statisticsService.save("test", account); /** * Then */ final BigDecimal expectedExpensesAmount = new BigDecimal("17.8861"); final BigDecimal expectedIncomesAmount = new BigDecimal("298.9802"); final BigDecimal expectedSavingAmount = new BigDecimal("1250"); final BigDecimal expectedNormalizedSalaryAmount = new BigDecimal("298.9802"); final BigDecimal expectedNormalizedVacationAmount = new BigDecimal("11.6361"); final BigDecimal expectedNormalizedGroceryAmount = new BigDecimal("6.25"); assertEquals(dataPoint.getId().getAccount(), "test"); assertEquals(dataPoint.getId().getDate(), Date.from(LocalDate.now().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant())); assertTrue(expectedExpensesAmount.compareTo(dataPoint.getStatistics().get(StatisticMetric.EXPENSES_AMOUNT)) == 0); assertTrue(expectedIncomesAmount.compareTo(dataPoint.getStatistics().get(StatisticMetric.INCOMES_AMOUNT)) == 0); assertTrue(expectedSavingAmount.compareTo(dataPoint.getStatistics().get(StatisticMetric.SAVING_AMOUNT)) == 0); ItemMetric salaryItemMetric = dataPoint.getIncomes().stream() .filter(i -> i.getTitle().equals(salary.getTitle())) .findFirst().get(); ItemMetric vacationItemMetric = dataPoint.getExpenses().stream() .filter(i -> i.getTitle().equals(vacation.getTitle())) .findFirst().get(); ItemMetric groceryItemMetric = dataPoint.getExpenses().stream() .filter(i -> i.getTitle().equals(grocery.getTitle())) .findFirst().get(); assertTrue(expectedNormalizedSalaryAmount.compareTo(salaryItemMetric.getAmount()) == 0); assertTrue(expectedNormalizedVacationAmount.compareTo(vacationItemMetric.getAmount()) == 0); assertTrue(expectedNormalizedGroceryAmount.compareTo(groceryItemMetric.getAmount()) == 0); assertEquals(rates, dataPoint.getRates()); verify(repository, times(1)).save(dataPoint); } } ================================================ FILE: statistics-service/src/test/resources/application.yml ================================================ hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 20000 spring: data: mongodb: database: piggymetrics port: 0 rates: url: https://api.exchangeratesapi.io ================================================ FILE: statistics-service/src/test/resources/bootstrap.yml ================================================ eureka: client: enabled: false ================================================ FILE: turbine-stream-service/Dockerfile ================================================ FROM java:8-jre MAINTAINER Chi Dov ADD ./target/turbine-stream-service.jar /app/ CMD ["java", "-Xmx200m", "-jar", "/app/turbine-stream-service.jar"] EXPOSE 8989 ================================================ FILE: turbine-stream-service/pom.xml ================================================ 4.0.0 turbine-stream-service 0.0.1-SNAPSHOT jar turbine-stream-service Turbine Stream Service com.piggymetrics piggymetrics 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-config org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-turbine-stream org.springframework.cloud spring-cloud-starter-stream-rabbit org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ${project.name} ================================================ FILE: turbine-stream-service/src/main/java/com/piggymetrics/turbine/TurbineStreamServiceApplication.java ================================================ package com.piggymetrics.turbine; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.turbine.stream.EnableTurbineStream; @SpringBootApplication @EnableTurbineStream @EnableDiscoveryClient public class TurbineStreamServiceApplication { public static void main(String[] args) { SpringApplication.run(TurbineStreamServiceApplication.class, args); } } ================================================ FILE: turbine-stream-service/src/main/resources/bootstrap.yml ================================================ spring: application: name: turbine-stream-service cloud: config: uri: http://config:8888 fail-fast: true password: ${CONFIG_SERVICE_PASSWORD} username: user ================================================ FILE: turbine-stream-service/src/test/java/com/piggymetrics/turbine/TurbineStreamServiceApplicationTests.java ================================================ package com.piggymetrics.turbine; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class TurbineStreamServiceApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: turbine-stream-service/src/test/resources/bootstrap.yml ================================================ eureka: client: enabled: false