├── .gitignore ├── LICENCE.md ├── README.md ├── application.main ├── application.test ├── docs ├── step1.md ├── step2.md ├── step3.md ├── step4.md ├── step5.md ├── step6.md ├── step7.md ├── step8.md └── step9.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── dogepool │ │ └── practicalrx │ │ ├── Main.java │ │ ├── controllers │ │ ├── AdminController.java │ │ ├── IndexController.java │ │ ├── PoolController.java │ │ ├── RateController.java │ │ ├── SearchController.java │ │ └── UserProfileController.java │ │ ├── domain │ │ ├── ExchangeRate.java │ │ ├── User.java │ │ ├── UserProfile.java │ │ └── UserStat.java │ │ ├── error │ │ ├── DogePoolException.java │ │ ├── Error.java │ │ ├── ErrorCategory.java │ │ └── ErrorHandler.java │ │ ├── internal │ │ └── config │ │ │ ├── CouchbaseStorageConfiguration.java │ │ │ ├── RestClientConfiguration.java │ │ │ └── VelocityPublicFieldUberspect.java │ │ ├── services │ │ ├── AdminService.java │ │ ├── CoinService.java │ │ ├── ExchangeRateService.java │ │ ├── HashrateService.java │ │ ├── PoolRateService.java │ │ ├── PoolService.java │ │ ├── RankingService.java │ │ ├── SearchService.java │ │ ├── StatService.java │ │ └── UserService.java │ │ └── views │ │ └── models │ │ ├── IndexModel.java │ │ └── MinerModel.java └── resources │ ├── static │ ├── css │ │ └── semantic.min.css │ └── images │ │ └── dogeError.jpg │ └── templates │ ├── error.vm │ ├── errorWithDetail.vm │ ├── index.vm │ └── miner.vm └── test ├── java └── org │ └── dogepool │ └── practicalrx │ └── controllers │ ├── AdminControllerTest.java │ ├── IndexControllerTest.java │ ├── PoolControllerTest.java │ ├── RateControllerTest.java │ ├── SearchControllerTest.java │ └── UserProfileControllerTest.java └── resources └── testResources.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | application.properties 4 | 5 | ### Maven ### 6 | target/ 7 | pom.xml.tag 8 | pom.xml.releaseBackup 9 | pom.xml.versionsBackup 10 | pom.xml.next 11 | release.properties 12 | dependency-reduced-pom.xml 13 | 14 | 15 | ### Java ### 16 | *.class 17 | 18 | # Mobile Tools for Java (J2ME) 19 | .mtj.tmp/ 20 | 21 | # Package Files # 22 | *.jar 23 | *.war 24 | *.ear 25 | 26 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 27 | hs_err_pid* 28 | 29 | 30 | ### Intellij ### 31 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 32 | 33 | *.iml 34 | 35 | ## Directory-based project format: 36 | .idea/ 37 | # if you remove the above rule, at least ignore the following: 38 | 39 | # User-specific stuff: 40 | # .idea/workspace.xml 41 | # .idea/tasks.xml 42 | # .idea/dictionaries 43 | 44 | # Sensitive or high-churn files: 45 | # .idea/dataSources.ids 46 | # .idea/dataSources.xml 47 | # .idea/sqlDataSources.xml 48 | # .idea/dynamic.xml 49 | # .idea/uiDesigner.xml 50 | 51 | # Gradle: 52 | # .idea/gradle.xml 53 | # .idea/libraries 54 | 55 | # Mongo Explorer plugin: 56 | # .idea/mongoSettings.xml 57 | 58 | ## File-based project format: 59 | *.ipr 60 | *.iws 61 | 62 | ## Plugin-specific files: 63 | 64 | # IntelliJ 65 | out/ 66 | 67 | # mpeltonen/sbt-idea plugin 68 | .idea_modules/ 69 | 70 | # JIRA plugin 71 | atlassian-ide-plugin.xml 72 | 73 | # Crashlytics plugin (for Android Studio and IntelliJ) 74 | com_crashlytics_export_strings.xml 75 | crashlytics.properties 76 | crashlytics-build.properties 77 | 78 | 79 | ### Eclipse ### 80 | *.pydevproject 81 | .metadata 82 | .gradle 83 | bin/ 84 | tmp/ 85 | *.tmp 86 | *.bak 87 | *.swp 88 | *~.nib 89 | local.properties 90 | .settings/ 91 | .loadpath 92 | 93 | # Eclipse Core 94 | .project 95 | 96 | # External tool builders 97 | .externalToolBuilders/ 98 | 99 | # Locally stored "Eclipse launch configurations" 100 | *.launch 101 | 102 | # CDT-specific 103 | .cproject 104 | 105 | # JDT-specific (Eclipse Java Development Tools) 106 | .classpath 107 | 108 | # PDT-specific 109 | .buildpath 110 | 111 | # sbteclipse plugin 112 | .target 113 | 114 | # TeXlipse plugin 115 | .texlipse 116 | 117 | 118 | ### OSX ### 119 | .DS_Store 120 | .AppleDouble 121 | .LSOverride 122 | 123 | # Icon must end with two \r 124 | Icon 125 | 126 | 127 | # Thumbnails 128 | ._* 129 | 130 | # Files that might appear in the root of a volume 131 | .DocumentRevisions-V100 132 | .fseventsd 133 | .Spotlight-V100 134 | .TemporaryItems 135 | .Trashes 136 | .VolumeIcon.icns 137 | 138 | # Directories potentially created on remote AFP share 139 | .AppleDB 140 | .AppleDesktop 141 | Network Trash Folder 142 | Temporary Items 143 | .apdisk 144 | 145 | 146 | ### Windows ### 147 | # Windows image file caches 148 | Thumbs.db 149 | ehthumbs.db 150 | 151 | # Folder config file 152 | Desktop.ini 153 | 154 | # Recycle Bin used on file shares 155 | $RECYCLE.BIN/ 156 | 157 | # Windows Installer files 158 | *.cab 159 | *.msi 160 | *.msm 161 | *.msp 162 | 163 | # Windows shortcuts 164 | *.lnk 165 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | Copyright 2015 Simon Baslé 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Practical RxJava Workshop 2 | # README 3 | This project is the skeleton (and solution) for the `PracticalRxjava` workshop. 4 | 5 | The aim of the workshop is to discover [`RxJava`](http://reactivex.io) through a practical example, a legacy application that we want to gradually migrate to a fully **asynchronous** and **reactive** app. 6 | 7 | The application is a `REST API`, based on `Spring Boot` and layered into `Controllers` and `Services`. Services call themselves, a database and even `external REST APIs`. 8 | 9 | > /!\ This project should only be cloned for the duration of the workshop. It can (and will) be recreated from [upstream](http://github.com/simonbasle/practicalRxOrigin) if feedback warrants it or new features are brought into the workshop. **Don't submit issues or PR here** but rather in the upstream [practicalRxOrigin repo](http://github.com/simonbasle/practicalRxOrigin). 10 | 11 | ### You are now doing [step1](docs/step1.md) 12 | 13 | ## Pre-requisites 14 | * The workshop application assumes there is a machine running the [`practicalRxExternal` project](https://github.com/simonbasle/practicalRxExternal) (usually the workshop's presenter). 15 | 16 | > **PracticalRxExternal**: a fake set of "external REST APIs" called by the legacy application. Some of these are unreliable and will simulate timeouts and delays. 17 | 18 | * The workshop can also make use of a [Couchbase database](http://www.couchbase.com). 19 | 20 | > `User`s can be stored as JSON in a Couchbase database, a key/value and document-oriented NoSQL database for which a fully reactive [Java SDK](http://github.com/couchbase/couchbase-java-client) exists. 21 | 22 | * Developers will need to copy and edit the configuration files 23 | * Copy `application.main` into `/src/main/resources/application.properties` 24 | * Copy `application.test` into `/src/test/resources/applications.properties` 25 | * Edit the copied files. Git will ignore them when switching branches. 26 | 27 | > There's a property to **change the IP** on which both the fake external APIs and the database will be running (usualy the workshop's presenter). 28 | > 29 | > IPs can also independently be modified (eg. if the attendee wants to run the database on his own machine). 30 | > 31 | > The need for a database can be deactivated (two `User`s will be created in memory). 32 | 33 | ## Running the Workshop 34 | This is a `Maven` project, developed and tested with the `IntelliJ` IDE. 35 | 36 | The REST API is fully functional and can be played with (eg. with a REST client like POSTMAN) by running the `Main` class. 37 | 38 | Controllers are unit tested (see `/src/test/java`) and attendees are encouraged to run tests from within the IDE to check that their modifications at each step are correct. 39 | 40 | > **Step 9** makes a deep change to the Controllers so **tests will change at this point**. One can checkout branch `step9pre` to see both the solution to step 8 and the new version of the tests in preparation for step 9. 41 | 42 | Each step is described in the ad hoc `stepX.md` document under `/docs`. 43 | 44 | One can **catch up if late or peek at the solution of each step** in its associated branch (eg. branch `solution/step2` contains the solution to step #2 described in `/docs/steps2.md` and can be used as a starting point for step #3). -------------------------------------------------------------------------------- /application.main: -------------------------------------------------------------------------------- 1 | #Change this to connect to the workshop's server 2 | #Note: you can change DB ip vs APIs ip independently below 3 | workshop.ip=127.0.0.1 4 | 5 | 6 | #DB Parameters 7 | ############## 8 | #set to false to completely disable using DB 9 | store.enable=true 10 | #set to false to disable using a query to find all users 11 | store.enableFindAll=true 12 | #the IP of the database 13 | store.nodes=${workshop.ip} 14 | #the bucket to use and associated password 15 | store.bucket=default 16 | store.bucket.password= 17 | 18 | 19 | #REST Client Parameters 20 | ####################### 21 | rest.client.readTimeoutMs=2000 22 | rest.client.connectTimeoutMs=800 23 | 24 | 25 | #External APIs Parameters 26 | ######################### 27 | #the IP used by all external APIs 28 | externalApi.ip=${workshop.ip} 29 | 30 | #avatar API 31 | avatar.api.ip=${externalApi.ip} 32 | avatar.api.port=8071 33 | avatar.api.baseUrl=http://${avatar.api.ip}:${avatar.api.port}/avatar 34 | 35 | #doge to dollar rate API 36 | doge.api.ip=${externalApi.ip} 37 | doge.api.port=8072 38 | doge.api.baseUrl=http://${doge.api.ip}:${doge.api.port} 39 | 40 | #free currency to currency rate API 41 | exchange.free.api.ip=${externalApi.ip} 42 | exchange.free.api.port=8073 43 | exchange.free.api.baseUrl=http://${exchange.free.api.ip}:${exchange.free.api.port} 44 | 45 | #reliable currency to currency rate API 46 | exchange.nonfree.api.ip=${externalApi.ip} 47 | exchange.nonfree.api.port=8074 48 | exchange.nonfree.api.baseUrl=http://${exchange.nonfree.api.ip}:${exchange.nonfree.api.port} 49 | 50 | 51 | #Templating Parameters 52 | ###################### 53 | spring.velocity.properties.runtime.introspector.uberspect=org.dogepool.practicalrx.internal.config.VelocityPublicFieldUberspect -------------------------------------------------------------------------------- /application.test: -------------------------------------------------------------------------------- 1 | #### TEST CONFIGURATION #### 2 | ############################ 3 | # Most notable has # 4 | # - store.enable false # 5 | # - readTimeoutMs 6000 # 6 | ############################ 7 | 8 | #Change this to connect to the workshop's server 9 | #Note: you can change DB ip vs APIs ip independently below 10 | workshop.ip=127.0.0.1 11 | 12 | 13 | #DB Parameters 14 | ############## 15 | #set to false to completely disable using DB 16 | store.enable=false 17 | #set to false to disable using a query to find all users 18 | store.enableFindAll=true 19 | #the IP of the database 20 | store.nodes=${workshop.ip} 21 | #the bucket to use and associated password 22 | store.bucket=default 23 | store.bucket.password= 24 | 25 | 26 | #REST Client Parameters 27 | ####################### 28 | rest.client.readTimeoutMs=6000 29 | rest.client.connectTimeoutMs=800 30 | 31 | 32 | #External APIs Parameters 33 | ######################### 34 | #the IP used by all external APIs 35 | externalApi.ip=${workshop.ip} 36 | 37 | #avatar API 38 | avatar.api.ip=${externalApi.ip} 39 | avatar.api.port=8071 40 | avatar.api.baseUrl=http://${avatar.api.ip}:${avatar.api.port}/avatar 41 | 42 | #doge to dollar rate API 43 | doge.api.ip=${externalApi.ip} 44 | doge.api.port=8072 45 | doge.api.baseUrl=http://${doge.api.ip}:${doge.api.port} 46 | 47 | #free currency to currency rate API 48 | exchange.free.api.ip=${externalApi.ip} 49 | exchange.free.api.port=8073 50 | exchange.free.api.baseUrl=http://${exchange.free.api.ip}:${exchange.free.api.port} 51 | 52 | #reliable currency to currency rate API 53 | exchange.nonfree.api.ip=${externalApi.ip} 54 | exchange.nonfree.api.port=8074 55 | exchange.nonfree.api.baseUrl=http://${exchange.nonfree.api.ip}:${exchange.nonfree.api.port} 56 | 57 | 58 | #Templating Parameters 59 | ###################### 60 | spring.velocity.properties.runtime.introspector.uberspect=org.dogepool.practicalrx.internal.config.VelocityPublicFieldUberspect -------------------------------------------------------------------------------- /docs/step1.md: -------------------------------------------------------------------------------- 1 | # Step 1 - Simple Creation 2 | Start from branch `master`, solution is in branch `solution/step1`. You can look at the history 3 | of the solution to see changes in the services and implied modifications separately. 4 | 5 | 6 | ## Migrate Services 7 | - AdminService 8 | - CoinService 9 | - HashrateService 10 | - PoolService 11 | 12 | ## Useful Operators 13 | - `Observable.just` 14 | - `Observable.from` 15 | - `Observable.create` (try it on PoolService.connect) 16 | - `map` 17 | 18 | ## How to easily (and naively) make things compile? 19 | By blocking on the `Observable`: 20 | 21 | - first chain a `toList()` if expected result is a `List` 22 | - chain `toBlocking()` 23 | - collect the result (or throw an `Exception`) with various operators 24 | - `take(n)` 25 | - `single()` 26 | - `first()` 27 | - `last()` 28 | - `???orDefault(T defaultValue)` 29 | 30 | A cleaner solution (going async all the way) will be presented later. -------------------------------------------------------------------------------- /docs/step2.md: -------------------------------------------------------------------------------- 1 | # Step 2 - Transform 2 | Start from branch `solution/step1`, solution is in branch `solution/step2`. 3 | 4 | ## Migrate Services 5 | - PoolRateService 6 | 7 | ## Useful Operators 8 | - `flatMap` 9 | - `reduce` -------------------------------------------------------------------------------- /docs/step3.md: -------------------------------------------------------------------------------- 1 | # Step 3 - Filter 2 | Start from branch `solution/step2`, solution is in branch `solution/step3`. 3 | 4 | ## Migrate Services 5 | - UserService 6 | - `findAll()` for now naively adapted (`Observable.from`) 7 | - compose on `findAll` for `getUser` / `getUserByLogin` 8 | - SearchService 9 | 10 | ## Useful Operators 11 | - `filter` 12 | - `take` 13 | - `flatMap` to asynchronously retrieve additional data needed for filter -------------------------------------------------------------------------------- /docs/step4.md: -------------------------------------------------------------------------------- 1 | # Step 4 - Count 2 | Start from branch `solution/step3`, solution is in branch `solution/step4`. 3 | 4 | ## Migrate Services 5 | - RankingService 6 | - use from for `rankByHashrate` / `rankByCoins` 7 | - complete migration for other methods 8 | 9 | ## Useful Operators 10 | - `takeUntil` 11 | - `count` 12 | - `take` -------------------------------------------------------------------------------- /docs/step5.md: -------------------------------------------------------------------------------- 1 | # Step 5 - Side Effects 2 | Start from branch `solution/step4`, solution is in branch `solution/step5`. 3 | 4 | ## Operators for Side Effects 5 | The various `doOnXXX` operators are dedicated to side effects: 6 | 7 | - `doOnNext` 8 | - `doOnError` 9 | - `doOnCompleted` 10 | - `doOnEach` 11 | 12 | ## Migrate Services 13 | - PoolService 14 | - add a line of log each time a user connects 15 | 16 | -------------------------------------------------------------------------------- /docs/step6.md: -------------------------------------------------------------------------------- 1 | # Step 6 - Combine 2 | Start from branch `solution/step5`, solution is in branch `solution/step6`. 3 | 4 | ## Operators (by difficulty) 5 | Simple: `concat` 6 | 7 | Intermediate: `merge` 8 | 9 | Advanced: `zip` 10 | 11 | ## Migrate Services 12 | - `StatService.getAllStats` 13 | - for each `User` 14 | - retrieve his hashrate 15 | - retrieve how many coins he mined 16 | - combine (zip) both and make a `UserStat` -------------------------------------------------------------------------------- /docs/step7.md: -------------------------------------------------------------------------------- 1 | # Step 007 - Live & Let Die (and Retry) 2 | Start from branch `solution/step6`, solution is in branch `solution/step7`. 3 | 4 | ## The Problem 5 | `StatService.lastBlockFoundBy` has an intermittent bug that causes it to crash with an `IndexOutOfBoundsException`. 6 | 7 | We'd like to retry the call when this happens to prevent this error. 8 | 9 | ## The Plan 10 | Make the method observable: 11 | 12 | - Have the index randomly generated inside an `Observable.defer`. 13 | - Have the generated index logged in a `doOnNext`. 14 | - Chain in a `flatMap` that calls `UserService.findAll()` and picks the user via `elementAt(i)`. 15 | 16 | Migrate code that uses this method as before. It still randomly fails. 17 | 18 | Make it automatically retry by chaining in the `retry()` operator. 19 | 20 | Edit the `PoolController.lastBlock()` method so it doesn't catch and recover. 21 | Execute test `PoolControllerTest.testLastBlock()` and verifies it succeeds, sometimes printing 22 | several "ELECTED" messages (the retry in action). 23 | 24 | ## Useful Operators 25 | * `defer` 26 | * `flatMap` 27 | * `elementAt` 28 | * `retry` -------------------------------------------------------------------------------- /docs/step8.md: -------------------------------------------------------------------------------- 1 | # Step 8 - Chain 2 | Start from branch `solution/step7`, solution is in branch `solution/step8`. 3 | 4 | ## Migrate Services 5 | - ExchangeRateService 6 | - calls two external APIs: doge-to-dollar rate and currency-to-currency exchange rate 7 | - two chained REST calls 8 | - combination of the two gives doge-to-anyCurrency 9 | 10 | ## Useful Operators 11 | - `Observable.create` 12 | - wrap the REST call on each subscription 13 | - the `HttpStatusCodeException` indicate error inside external API, transform into `DogePoolException` but forward the `message` and keep the `status code`. 14 | - the other `RestClientException` most probably indicate a timeout, wrap them into `DogePoolException` as well, with `418` status code. 15 | 16 | - `zipWith` 17 | - similar to `zip` but called from stream A directly instead of statically 18 | - combine both rates to get the final one 19 | 20 | # NEW REQUIREMENTS 21 | > the free exchange rate API crashes too often, make it so we switch to an alternative non-free API when it’s the case 22 | > 23 | > as a bonus, track costs of these non-free calls 24 | > 25 | >-- the Product Owner 26 | 27 | ## The Plan 28 | **AdminService**: 29 | add method+variable to track cost for this month 30 | 31 | **ExchangeRateService**: 32 | add a rate retrieval method similar to current one, 33 | but use endpoint from `exchange.nonfree.api.baseUrl` 34 | 35 | **upon error** 36 | switch thanks to `OnErrorResumeNext` 37 | 38 | **use side effects** 39 | to log when we switch & track the cost -------------------------------------------------------------------------------- /docs/step9.md: -------------------------------------------------------------------------------- 1 | # Step 9 - Clean-up Controllers 2 | Start from branch `solution/step9pre`, solution is in branch `solution/step9`. 3 | 4 | The starting branch changes the tests to account for the code migrating to a fully asynchronous one. 5 | 6 | ## Asynchronous Response 7 | **prepare a `DeferredResult`** 8 | that will be returned without blocking. 9 | Default status is `200 - OK` on a success, use a `DeferredResult>` to customize that. 10 | 11 | **subscribe on the `Observable`** 12 | to inject results or errors into the `DeferredResult`. 13 | 14 | * `onNext`: inject `T` via `setResult` 15 | * `onError`: create a `DogePoolException` and inject it via `setErrorResult` 16 | 17 | ## Testing 18 | - make sure that the endpoint is **asynchronous** 19 | - perform the request using `mockMvc`, but finish with a `andReturn()` to get a `MvcResult` 20 | - assert `status().isOk()` and `request().asyncStarted()` 21 | 22 | - **trigger** the async processing using the previously acquired *mvcResult* 23 | - `mockMvc.perform(asyncDispatch(mvcResult))` 24 | 25 | - from this point **assert as before** on the status, content, etc... 26 | 27 | ## Migrate Services 28 | - AdminController *(simple)* 29 | - IndexController *(trickier)* 30 | - use `zip`, `flatMap`, `single`… to detect bad users 31 | - UserProfileController *(also trickier)* 32 | 33 | ## A Way to Generalize this 34 | using `ReturnValueHandler` and a simple adapter of `Observable` to `DeferredResult`, you wouldn't need to make code changes everywhere. 35 | 36 | each time an `Observable` is returned, it’ll be converted into this adapter 37 | > `WebAsyncUtils.getAsyncManager(...).startDeferredResultProcessing(...)` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.dogepool 8 | practicalrx 9 | 1.0 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 1.2.2.RELEASE 15 | 16 | 17 | 18 | 19 | 20 | org.apache.maven.plugins 21 | maven-compiler-plugin 22 | 23 | 1.8 24 | 1.8 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-web 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-velocity 39 | 40 | 41 | 42 | org.apache.httpcomponents 43 | httpclient 44 | 4.3.6 45 | 46 | 47 | 48 | io.reactivex 49 | rxjava 50 | 1.0.15 51 | 52 | 53 | 54 | 55 | com.couchbase.client 56 | java-client 57 | 2.2.4 58 | 59 | 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-test 64 | 65 | 66 | org.skyscreamer 67 | jsonassert 68 | 1.2.3 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/Main.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx; 2 | 3 | import java.io.File; 4 | import java.util.List; 5 | 6 | import com.couchbase.client.java.Bucket; 7 | import com.couchbase.client.java.document.JsonDocument; 8 | import org.dogepool.practicalrx.domain.User; 9 | import org.dogepool.practicalrx.domain.UserStat; 10 | import org.dogepool.practicalrx.services.ExchangeRateService; 11 | import org.dogepool.practicalrx.services.PoolRateService; 12 | import org.dogepool.practicalrx.services.PoolService; 13 | import org.dogepool.practicalrx.services.RankingService; 14 | import org.dogepool.practicalrx.services.UserService; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.CommandLineRunner; 17 | import org.springframework.boot.SpringApplication; 18 | import org.springframework.boot.autoconfigure.SpringBootApplication; 19 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 20 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 21 | import org.springframework.context.ConfigurableApplicationContext; 22 | import org.springframework.context.annotation.Bean; 23 | import org.springframework.core.annotation.Order; 24 | 25 | @SpringBootApplication 26 | public class Main { 27 | 28 | public static void main(String[] args) { 29 | checkConfig(); 30 | ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args); 31 | } 32 | 33 | private static void checkConfig() { 34 | File mainConfig = new File("src/main/resources/application.properties"); 35 | File testConfig = new File("src/test/resources/application.properties"); 36 | 37 | System.out.println(mainConfig.isFile() + " " + testConfig.isFile()); 38 | 39 | if (!mainConfig.isFile() || !testConfig.isFile()) { 40 | throw new IllegalStateException("\n\n========PLEASE CONFIGURE PROJECT========" + 41 | "\nApplication configuration not found, have you:" + 42 | "\n\t - copied \"application.main\" to \"src/main/resources/application.properties\"?" + 43 | "\n\t - copied \"application.test\" to \"src/test/resources/application.properties\"?" + 44 | "\n\t - edited these files with the correct configuration?" + 45 | "\n========================================\n"); 46 | } 47 | } 48 | 49 | @Bean 50 | @ConditionalOnBean(value = Bucket.class) 51 | @Order(value = 1) 52 | CommandLineRunner userCreation(Bucket couchbaseBucket) { 53 | return args -> { 54 | JsonDocument u1 = JsonDocument.create(String.valueOf(User.USER.id), User.USER.toJsonObject()); 55 | JsonDocument u2 = JsonDocument.create(String.valueOf(User.OTHERUSER.id), User.OTHERUSER.toJsonObject()); 56 | couchbaseBucket.upsert(u1); 57 | couchbaseBucket.upsert(u2); 58 | }; 59 | } 60 | 61 | @Bean 62 | @Order(value = 2) 63 | CommandLineRunner commandLineRunner(UserService userService, RankingService rankinService, 64 | PoolService poolService, PoolRateService poolRateService, ExchangeRateService exchangeRateService) { 65 | return args -> { 66 | User user = userService.getUser(0); 67 | //connect USER automatically 68 | boolean connected = poolService.connectUser(user); 69 | 70 | //gather data 71 | List hashLadder = rankinService.getLadderByHashrate(); 72 | List coinsLadder = rankinService.getLadderByCoins(); 73 | String poolName = poolService.poolName(); 74 | int miningUserCount = poolService.miningUsers().size(); 75 | double poolRate = poolRateService.poolGigaHashrate(); 76 | 77 | //display welcome screen in console 78 | System.out.println("Welcome to " + poolName + " dogecoin mining pool!"); 79 | System.out.println(miningUserCount + " users currently mining, for a global hashrate of " 80 | + poolRate + " GHash/s"); 81 | 82 | try { 83 | Double dogeToDollar = exchangeRateService.dogeToCurrencyExchangeRate("USD"); 84 | System.out.println("1 DOGE = " + dogeToDollar + "$"); 85 | } catch (Exception e) { 86 | System.out.println("1 DOGE = ??$, couldn't get the exchange rate - " + e); 87 | } 88 | try { 89 | Double dogeToEuro = exchangeRateService.dogeToCurrencyExchangeRate("EUR"); 90 | System.out.println("1 DOGE = " + dogeToEuro + "€"); 91 | } catch (Exception e) { 92 | System.out.println("1 DOGE = ??€, couldn't get the exchange rate - " + e); 93 | } 94 | 95 | System.out.println("\n----- TOP 10 Miners by Hashrate -----"); 96 | int count = 1; 97 | for (UserStat userStat : hashLadder) { 98 | System.out.println(count++ + ": " + userStat.user.nickname + ", " + userStat.hashrate + " GHash/s"); 99 | } 100 | 101 | System.out.println("\n----- TOP 10 Miners by Coins Found -----"); 102 | count = 1; 103 | for (UserStat userStat : coinsLadder) { 104 | System.out.println(count++ + ": " + userStat.user.nickname + ", " + userStat.totalCoinsMined + " dogecoins"); 105 | } 106 | }; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/controllers/AdminController.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import java.math.BigInteger; 4 | import java.time.LocalDate; 5 | import java.time.Month; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import org.dogepool.practicalrx.domain.User; 11 | import org.dogepool.practicalrx.error.*; 12 | import org.dogepool.practicalrx.error.Error; 13 | import org.dogepool.practicalrx.services.AdminService; 14 | import org.dogepool.practicalrx.services.PoolService; 15 | import org.dogepool.practicalrx.services.UserService; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.http.HttpStatus; 18 | import org.springframework.http.MediaType; 19 | import org.springframework.http.ResponseEntity; 20 | import org.springframework.web.bind.annotation.PathVariable; 21 | import org.springframework.web.bind.annotation.RequestMapping; 22 | import org.springframework.web.bind.annotation.RequestMethod; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | @RestController 26 | @RequestMapping(value = "/admin", produces = MediaType.APPLICATION_JSON_VALUE) 27 | public class AdminController { 28 | 29 | @Autowired 30 | private UserService userService; 31 | 32 | @Autowired 33 | private PoolService poolService; 34 | 35 | @Autowired 36 | private AdminService adminService; 37 | 38 | @RequestMapping(method = RequestMethod.POST, value = "/mining/{id}", consumes = MediaType.ALL_VALUE) 39 | public ResponseEntity registerMiningUser(@PathVariable("id") long id) { 40 | User user = userService.getUser(id); 41 | if (user != null) { 42 | boolean connected = poolService.connectUser(user); 43 | List miningUsers = poolService.miningUsers(); 44 | return new ResponseEntity<>(miningUsers, HttpStatus.ACCEPTED); 45 | } else { 46 | throw new DogePoolException("User cannot mine, not authenticated", Error.BAD_USER, HttpStatus.NOT_FOUND); 47 | } 48 | } 49 | 50 | @RequestMapping(method = RequestMethod.DELETE, value = "mining/{id}", consumes = MediaType.ALL_VALUE) 51 | public ResponseEntity deregisterMiningUser(@PathVariable("id") long id) { 52 | User user = userService.getUser(id); 53 | if (user != null) { 54 | boolean disconnected = poolService.disconnectUser(user); 55 | List miningUsers = poolService.miningUsers(); 56 | return new ResponseEntity<>(miningUsers, HttpStatus.ACCEPTED); 57 | } else { 58 | throw new DogePoolException("User is not mining, not authenticated", Error.BAD_USER, HttpStatus.NOT_FOUND); 59 | } 60 | } 61 | 62 | @RequestMapping("/cost/{year}-{month}") 63 | public Map cost(@PathVariable int year, @PathVariable int month) { 64 | Month monthEnum = Month.of(month); 65 | return cost(year, monthEnum); 66 | } 67 | 68 | @RequestMapping("/cost") 69 | public Map cost() { 70 | LocalDate now = LocalDate.now(); 71 | return cost(now.getYear(), now.getMonth()); 72 | } 73 | 74 | @RequestMapping("/cost/{year}/{month}") 75 | protected Map cost(@PathVariable int year, @PathVariable Month month) { 76 | BigInteger cost = adminService.costForMonth(year, month); 77 | 78 | Map json = new HashMap<>(); 79 | json.put("month", month + " " + year); 80 | json.put("cost", cost); 81 | json.put("currency", "USD"); 82 | json.put("currencySign", "$"); 83 | return json; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/controllers/IndexController.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import java.util.Map; 4 | 5 | import org.dogepool.practicalrx.domain.UserStat; 6 | import org.dogepool.practicalrx.services.ExchangeRateService; 7 | import org.dogepool.practicalrx.services.PoolRateService; 8 | import org.dogepool.practicalrx.services.PoolService; 9 | import org.dogepool.practicalrx.services.RankingService; 10 | import org.dogepool.practicalrx.views.models.IndexModel; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Controller; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | 15 | /** 16 | * A utility controller that displays the welcome message as HTML on root endpoint. 17 | */ 18 | @Controller 19 | public class IndexController { 20 | 21 | @Autowired 22 | private RankingService rankService; 23 | 24 | @Autowired 25 | private PoolService poolService; 26 | 27 | @Autowired 28 | private PoolRateService poolRateService; 29 | 30 | @Autowired 31 | private ExchangeRateService exchangeRateService; 32 | 33 | @RequestMapping("/") 34 | public String index(Map model) { 35 | //prepare a model 36 | IndexModel idxModel = new IndexModel(); 37 | idxModel.setHashLadder(rankService.getLadderByHashrate()); 38 | idxModel.setCoinsLadder(rankService.getLadderByCoins()); 39 | idxModel.setPoolName(poolService.poolName()); 40 | idxModel.setMiningUserCount(poolService.miningUsers().size()); 41 | idxModel.setGigaHashrate(poolRateService.poolGigaHashrate()); 42 | try { 43 | Double dogeToDollar = exchangeRateService.dogeToCurrencyExchangeRate("USD"); 44 | idxModel.setDogeToUsdMessage("1 DOGE = " + dogeToDollar + "$"); 45 | } catch (Exception e) { 46 | idxModel.setDogeToUsdMessage("1 DOGE = ??$, couldn't get the exchange rate - " + e.getMessage()); 47 | } 48 | try { 49 | Double dogeToEuro = exchangeRateService.dogeToCurrencyExchangeRate("EUR"); 50 | idxModel.setDogeToEurMessage("1 DOGE = " + dogeToEuro + "€"); 51 | } catch (Exception e) { 52 | idxModel.setDogeToEurMessage("1 DOGE = ??€, couldn't get the exchange rate - " + e.getMessage()); 53 | } 54 | 55 | //populate the model and call the template 56 | model.put("model", idxModel); 57 | return "index"; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/controllers/PoolController.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import java.time.Duration; 4 | import java.time.LocalDateTime; 5 | import java.time.format.DateTimeFormatter; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import org.dogepool.practicalrx.domain.User; 11 | import org.dogepool.practicalrx.domain.UserStat; 12 | import org.dogepool.practicalrx.services.PoolRateService; 13 | import org.dogepool.practicalrx.services.PoolService; 14 | import org.dogepool.practicalrx.services.RankingService; 15 | import org.dogepool.practicalrx.services.StatService; 16 | import org.dogepool.practicalrx.services.UserService; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.http.MediaType; 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | import org.springframework.web.bind.annotation.RestController; 21 | 22 | @RestController 23 | @RequestMapping(value = "/pool", produces = MediaType.APPLICATION_JSON_VALUE) 24 | public class PoolController { 25 | 26 | @Autowired 27 | private UserService userService; 28 | 29 | @Autowired 30 | private RankingService rankingService; 31 | 32 | @Autowired 33 | private PoolService poolService; 34 | 35 | @Autowired 36 | private PoolRateService poolRateService; 37 | 38 | @Autowired 39 | private StatService statService; 40 | 41 | @RequestMapping("/ladder/hashrate") 42 | public List ladderByHashrate() { 43 | return rankingService.getLadderByHashrate(); 44 | } 45 | 46 | @RequestMapping("/ladder/coins") 47 | public List ladderByCoins() { 48 | return rankingService.getLadderByCoins(); 49 | } 50 | 51 | @RequestMapping("/hashrate") 52 | public Map globalHashRate() { 53 | Map json = new HashMap<>(2); 54 | double ghashrate = poolRateService.poolGigaHashrate(); 55 | if (ghashrate < 1) { 56 | json.put("unit", "MHash/s"); 57 | json.put("hashrate", ghashrate * 100d); 58 | } else { 59 | json.put("unit", "GHash/s"); 60 | json.put("hashrate", ghashrate); 61 | } 62 | return json; 63 | } 64 | 65 | @RequestMapping("/miners") 66 | public Map miners() { 67 | int allUsers = userService.findAll().size(); 68 | int miningUsers = poolService.miningUsers().size(); 69 | Map json = new HashMap<>(2); 70 | json.put("totalUsers", allUsers); 71 | json.put("totalMiningUsers", miningUsers); 72 | return json; 73 | } 74 | 75 | @RequestMapping("/miners/active") 76 | public List activeMiners() { 77 | return poolService.miningUsers(); 78 | } 79 | 80 | @RequestMapping("/lastblock") 81 | public Map lastBlock() { 82 | LocalDateTime found = statService.lastBlockFoundDate(); 83 | Duration foundAgo = Duration.between(found, LocalDateTime.now()); 84 | 85 | User foundBy; 86 | try { 87 | foundBy = statService.lastBlockFoundBy(); 88 | } catch (IndexOutOfBoundsException e) { 89 | System.err.println("WARNING: StatService failed to return the last user to find a coin"); 90 | foundBy = new User(-1, "BAD USER", "Bad User from StatService, please ignore", "", null); 91 | } 92 | Map json = new HashMap<>(2); 93 | json.put("foundOn", found.format(DateTimeFormatter.ISO_DATE_TIME)); 94 | json.put("foundAgo", foundAgo.toMinutes()); 95 | json.put("foundBy", foundBy); 96 | return json; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/controllers/RateController.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import org.dogepool.practicalrx.domain.ExchangeRate; 4 | import org.dogepool.practicalrx.error.*; 5 | import org.dogepool.practicalrx.error.Error; 6 | import org.dogepool.practicalrx.services.ExchangeRateService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | @RestController 15 | @RequestMapping(value = "/rate", produces = MediaType.APPLICATION_JSON_VALUE) 16 | public class RateController { 17 | 18 | @Autowired 19 | private ExchangeRateService service; 20 | 21 | @RequestMapping("{moneyTo}") 22 | public ExchangeRate rate(@PathVariable String moneyTo) { 23 | Double exchange = service.dogeToCurrencyExchangeRate(moneyTo); 24 | if (exchange == null) { 25 | throw new DogePoolException("Cannot find rate for " + moneyTo, Error.BAD_CURRENCY, HttpStatus.NOT_FOUND); 26 | } 27 | return new ExchangeRate("DOGE", moneyTo, exchange); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/controllers/SearchController.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import java.util.List; 4 | 5 | import org.dogepool.practicalrx.domain.User; 6 | import org.dogepool.practicalrx.domain.UserStat; 7 | import org.dogepool.practicalrx.services.SearchService; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | @RestController 15 | @RequestMapping(value = "/search", produces = MediaType.APPLICATION_JSON_VALUE) 16 | public class SearchController { 17 | 18 | @Autowired 19 | private SearchService service; 20 | 21 | @RequestMapping("user/{pattern}") 22 | public List searchByName(@PathVariable String pattern) { 23 | return service.findByName(pattern); 24 | } 25 | 26 | @RequestMapping("user/coins/{minCoins}") 27 | public List searchByCoins(@PathVariable long minCoins) { 28 | return this.searchByCoins(minCoins, -1L); 29 | } 30 | 31 | @RequestMapping("user/coins/{minCoins}/{maxCoins}") 32 | private List searchByCoins(@PathVariable long minCoins, @PathVariable long maxCoins) { 33 | return service.findByCoins(minCoins, maxCoins); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/controllers/UserProfileController.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import java.util.Map; 4 | 5 | import org.dogepool.practicalrx.domain.User; 6 | import org.dogepool.practicalrx.domain.UserProfile; 7 | import org.dogepool.practicalrx.error.*; 8 | import org.dogepool.practicalrx.error.Error; 9 | import org.dogepool.practicalrx.services.*; 10 | import org.dogepool.practicalrx.views.models.MinerModel; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.stereotype.Controller; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.ResponseBody; 20 | import org.springframework.web.client.RestTemplate; 21 | import org.springframework.web.context.request.async.DeferredResult; 22 | 23 | @Controller(value = "/miner") 24 | public class UserProfileController { 25 | 26 | @Value(value = "${avatar.api.baseUrl}") 27 | private String avatarBaseUrl; 28 | 29 | @Autowired 30 | private UserService userService; 31 | 32 | @Autowired 33 | private RankingService rankingService; 34 | 35 | @Autowired 36 | private HashrateService hashrateService; 37 | 38 | @Autowired 39 | private CoinService coinService; 40 | 41 | @Autowired 42 | private RestTemplate restTemplate; 43 | 44 | @RequestMapping(value = "/miner/{id}", produces = MediaType.APPLICATION_JSON_VALUE) 45 | public DeferredResult profile(@PathVariable int id) { 46 | DeferredResult deferred = new DeferredResult<>(90000); 47 | User user = userService.getUser(id); 48 | if (user == null) { 49 | deferred.setErrorResult(new DogePoolException("Unknown miner", Error.UNKNOWN_USER, HttpStatus.NOT_FOUND)); 50 | return deferred; 51 | } else { 52 | //find the avatar's url 53 | ResponseEntity avatarResponse = restTemplate.getForEntity(avatarBaseUrl + "/" + user.avatarId, Map.class); 54 | if (avatarResponse.getStatusCode().is2xxSuccessful()) { 55 | Map avatarInfo = avatarResponse.getBody(); 56 | String avatarUrl = (String) avatarInfo.get("large"); 57 | String smallAvatarUrl = (String) avatarInfo.get("small"); 58 | 59 | //complete with other information 60 | double hash = hashrateService.hashrateFor(user); 61 | long rankByHash = rankingService.rankByHashrate(user); 62 | long rankByCoins = rankingService.rankByCoins(user); 63 | long coins = coinService.totalCoinsMinedBy(user); 64 | 65 | deferred.setResult(new UserProfile(user, hash, coins, avatarUrl, smallAvatarUrl, rankByHash, rankByCoins)); 66 | return deferred; 67 | } else { 68 | deferred.setErrorResult(new DogePoolException("Unable to get avatar info", Error.UNREACHABLE_SERVICE, 69 | avatarResponse.getStatusCode())); 70 | return deferred; 71 | } 72 | } 73 | } 74 | 75 | @RequestMapping(value = "/miner/{id}", produces = MediaType.TEXT_HTML_VALUE) 76 | public DeferredResult miner(Map model, @PathVariable int id) { 77 | DeferredResult stringResponse = new DeferredResult<>(90000); 78 | User user = userService.getUser(id); 79 | if (user == null) { 80 | stringResponse.setErrorResult(new DogePoolException("Unknown miner", Error.UNKNOWN_USER, HttpStatus.NOT_FOUND)); 81 | return stringResponse; 82 | } else { 83 | //find the avatar's url 84 | ResponseEntity avatarResponse = restTemplate.getForEntity(avatarBaseUrl + "/" + user.avatarId, Map.class); 85 | if (avatarResponse.getStatusCode().is2xxSuccessful()) { 86 | Map avatarInfo = avatarResponse.getBody(); 87 | String avatarUrl = (String) avatarInfo.get("large"); 88 | String smallAvatarUrl = (String) avatarInfo.get("small"); 89 | 90 | //complete with other information 91 | double hash = hashrateService.hashrateFor(user); 92 | long rankByHash = rankingService.rankByHashrate(user); 93 | long rankByCoins = rankingService.rankByCoins(user); 94 | long coins = coinService.totalCoinsMinedBy(user); 95 | 96 | UserProfile profile = new UserProfile(user, hash, coins, avatarUrl, smallAvatarUrl, rankByHash, rankByCoins); 97 | MinerModel minerModel = new MinerModel(); 98 | minerModel.setAvatarUrl(profile.avatarUrl); 99 | minerModel.setSmallAvatarUrl(profile.smallAvatarUrl); 100 | minerModel.setBio(user.bio); 101 | minerModel.setDisplayName(user.displayName); 102 | minerModel.setNickname(user.nickname); 103 | minerModel.setRankByCoins(profile.rankByCoins); 104 | minerModel.setRankByHash(profile.rankByHash); 105 | model.put("minerModel", minerModel); 106 | stringResponse.setResult("miner"); 107 | 108 | return stringResponse; 109 | } else { 110 | stringResponse.setErrorResult(new DogePoolException("Unable to get avatar info", Error.UNREACHABLE_SERVICE, 111 | avatarResponse.getStatusCode())); 112 | return stringResponse; 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/domain/ExchangeRate.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.domain; 2 | 3 | public class ExchangeRate { 4 | 5 | public final String moneyCodeFrom; 6 | 7 | public final String moneyCodeTo; 8 | 9 | public final double exchangeRate; 10 | 11 | public ExchangeRate(String moneyCodeFrom, String moneyCodeTo, double exchangeRate) { 12 | this.moneyCodeFrom = moneyCodeFrom; 13 | this.moneyCodeTo = moneyCodeTo; 14 | this.exchangeRate = exchangeRate; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/domain/User.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.domain; 2 | 3 | import com.couchbase.client.deps.com.fasterxml.jackson.databind.util.JSONPObject; 4 | import com.couchbase.client.java.document.json.JsonObject; 5 | 6 | public class User { 7 | 8 | public static final User USER = new User(0L, "user0", "Test User", "Story of my life.\nEnd of Story.", "12434"); 9 | public static final User OTHERUSER = new User(1L, "richUser", "Richie Rich", "I'm rich I have dogecoin", "45678"); 10 | 11 | public final long id; 12 | public final String nickname; 13 | public final String displayName; 14 | public final String bio; 15 | public final String avatarId; 16 | public final String type = "user"; 17 | 18 | public User(long id, String nickname, String displayName, String bio, String avatarId) { 19 | this.id = id; 20 | this.nickname = nickname; 21 | this.displayName = displayName; 22 | this.bio = bio; 23 | this.avatarId = avatarId; 24 | } 25 | 26 | public JsonObject toJsonObject(){ 27 | JsonObject jso = JsonObject.create(); 28 | jso.put("id",id); 29 | jso.put("nickname",nickname); 30 | jso.put("displayName", displayName); 31 | jso.put("bio", bio); 32 | jso.put("avatarId", avatarId); 33 | jso.put("type", "user"); 34 | return jso; 35 | } 36 | 37 | public static User fromJsonObject(JsonObject jso){ 38 | return new User(jso.getInt("id"), jso.getString("nickname"), jso.getString("displayName"), jso.getString("bio"), 39 | jso.getString("avatarId")); 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) { 45 | return true; 46 | } 47 | if (o == null || getClass() != o.getClass()) { 48 | return false; 49 | } 50 | 51 | User user = (User) o; 52 | 53 | if (id != user.id) { 54 | return false; 55 | } 56 | if (!nickname.equals(user.nickname)) { 57 | return false; 58 | } 59 | 60 | return true; 61 | } 62 | 63 | @Override 64 | public int hashCode() { 65 | int result = (int) (id ^ (id >>> 32)); 66 | result = 31 * result + nickname.hashCode(); 67 | return result; 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return displayName + " (" + nickname + ")"; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/domain/UserProfile.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.domain; 2 | 3 | public class UserProfile extends UserStat { 4 | 5 | public final String avatarUrl; 6 | 7 | public final String smallAvatarUrl; 8 | 9 | public final long rankByHash; 10 | 11 | public final long rankByCoins; 12 | 13 | public UserProfile(User user, double hashrate, long totalCoinsMined, String avatarUrl, String smallAvatarUrl, 14 | long rankByHash, 15 | long rankByCoins) { 16 | super(user, hashrate, totalCoinsMined); 17 | this.avatarUrl = avatarUrl; 18 | this.smallAvatarUrl = smallAvatarUrl; 19 | this.rankByHash = rankByHash; 20 | this.rankByCoins = rankByCoins; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/domain/UserStat.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.domain; 2 | 3 | /** 4 | * A class that combines a {@link User User's} information with his/her stats, 5 | * like last known hashrate and total number of dogecoins mined since joining the pool. 6 | */ 7 | public class UserStat { 8 | 9 | public final User user; 10 | public final double hashrate; 11 | public final long totalCoinsMined; 12 | 13 | public UserStat(User user, double hashrate, long totalCoinsMined) { 14 | this.user = user; 15 | this.hashrate = hashrate; 16 | this.totalCoinsMined = totalCoinsMined; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/error/DogePoolException.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.error; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | /** 6 | * An exception that the dogepool API can raise in case of a problem (see {@link ErrorCategory}). 7 | */ 8 | public class DogePoolException extends RuntimeException { 9 | 10 | public final HttpStatus httpStatus; 11 | 12 | public final ErrorCategory errorCategory; 13 | 14 | public final int errorCode; 15 | 16 | public DogePoolException(String message, Error error, HttpStatus httpStatus) { 17 | this(message, error.code, error.category, httpStatus, null); 18 | } 19 | 20 | public DogePoolException(String message, Error error, HttpStatus httpStatus, Throwable cause) { 21 | this(message, error.code, error.category, httpStatus, cause); 22 | } 23 | 24 | public DogePoolException(String message, int errorCode, ErrorCategory errorCategory, HttpStatus httpStatus, 25 | Throwable cause) { 26 | super(message, cause); 27 | this.httpStatus = httpStatus; 28 | this.errorCategory = errorCategory; 29 | this.errorCode = errorCode; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) { 35 | return true; 36 | } 37 | if (o == null || getClass() != o.getClass()) { 38 | return false; 39 | } 40 | 41 | DogePoolException that = (DogePoolException) o; 42 | 43 | if (errorCode != that.errorCode) { 44 | return false; 45 | } 46 | if (httpStatus != that.httpStatus) { 47 | return false; 48 | } 49 | return errorCategory == that.errorCategory; 50 | 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | int result = httpStatus.hashCode(); 56 | result = 31 * result + errorCategory.hashCode(); 57 | result = 31 * result + errorCode; 58 | return result; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/error/Error.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.error; 2 | 3 | public enum Error { 4 | 5 | UNREACHABLE_SERVICE(0, ErrorCategory.EXTERNAL_SERVICES), 6 | BAD_USER(1, ErrorCategory.AUTHENTICATION), 7 | BAD_CURRENCY(2, ErrorCategory.EXCHANGE_RATE), 8 | UNKNOWN_USER(3, ErrorCategory.USERS), 9 | RANK_HASH(4, ErrorCategory.RANKING), 10 | RANK_COIN(5, ErrorCategory.RANKING), 11 | DATABASE(100, ErrorCategory.EXTERNAL_SERVICES); 12 | 13 | public final int code; 14 | 15 | public final ErrorCategory category; 16 | 17 | private Error(int code, ErrorCategory category) { 18 | this.code = code; 19 | this.category = category; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/error/ErrorCategory.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.error; 2 | 3 | /** 4 | * Enum of the domain error codes that the API can raise. 5 | */ 6 | public enum ErrorCategory { 7 | 8 | AUTHENTICATION, 9 | EXTERNAL_SERVICES, 10 | USERS, 11 | RANKING, 12 | EXCHANGE_RATE 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/error/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.error; 2 | 3 | import java.net.SocketTimeoutException; 4 | 5 | import javax.servlet.http.HttpServletResponse; 6 | 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.ControllerAdvice; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.client.HttpClientErrorException; 11 | import org.springframework.web.servlet.ModelAndView; 12 | 13 | /** 14 | * The global controller advice for handling errors and returning appropriate JSON. 15 | */ 16 | @ControllerAdvice 17 | public class ErrorHandler { 18 | 19 | @ExceptionHandler(DogePoolException.class) 20 | public ModelAndView dogePoolGenericHandler(DogePoolException e, HttpServletResponse response) { 21 | response.setStatus(e.httpStatus.value()); 22 | ModelAndView mav = new ModelAndView("errorWithDetail"); 23 | mav.addObject("httpStatus", e.httpStatus); 24 | mav.addObject("errorCategory", e.errorCategory); 25 | mav.addObject("msg", e.getMessage()); 26 | mav.addObject("errorCode", e.errorCode); 27 | //FIXME the exception could be logged here 28 | return mav; 29 | } 30 | 31 | @ExceptionHandler(HttpClientErrorException.class) 32 | public ModelAndView externalCallErrorHandler(HttpClientErrorException e, HttpServletResponse response) { 33 | response.setStatus(HttpStatus.FAILED_DEPENDENCY.value()); 34 | ModelAndView mav = new ModelAndView("errorWithDetail"); 35 | mav.addObject("httpStatus", response.getStatus()); 36 | mav.addObject("errorCategory", ErrorCategory.EXTERNAL_SERVICES); 37 | mav.addObject("msg", "External failure: " + e.getResponseBodyAsString()); 38 | mav.addObject("errorCode", "EXTERNAL_HTTP_" + e.getStatusCode()); 39 | //FIXME the exception could be logged here 40 | return mav; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/internal/config/CouchbaseStorageConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.internal.config; 2 | 3 | import com.couchbase.client.java.Bucket; 4 | import com.couchbase.client.java.Cluster; 5 | import com.couchbase.client.java.CouchbaseCluster; 6 | import com.couchbase.client.java.env.CouchbaseEnvironment; 7 | import com.couchbase.client.java.env.DefaultCouchbaseEnvironment; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | 14 | @Configuration 15 | public class CouchbaseStorageConfiguration { 16 | 17 | @Value("${store.enableFindAll:false}") 18 | private boolean useCouchbaseForFindAll; 19 | 20 | @Bean(destroyMethod = "disconnect") 21 | @ConditionalOnProperty("store.enable") 22 | public Cluster couchbaseCluster( @Value("#{'${store.nodes:127.0.0.1}'.split(',')}") String... nodes) { 23 | CouchbaseEnvironment env = DefaultCouchbaseEnvironment.builder() 24 | .queryEnabled(useCouchbaseForFindAll) 25 | .build(); 26 | return CouchbaseCluster.create(env, nodes); 27 | } 28 | 29 | @Bean 30 | @Autowired 31 | @ConditionalOnProperty("store.enable") 32 | public Bucket couchbaseBucket( Cluster cluster, 33 | @Value("${store.bucket:default}") String bucket, 34 | @Value("${store.bucket.password:}") String password) { 35 | 36 | return cluster.openBucket(bucket, password); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/internal/config/RestClientConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.internal.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.http.client.ClientHttpRequestFactory; 7 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 8 | import org.springframework.web.client.RestTemplate; 9 | 10 | @Configuration 11 | public class RestClientConfiguration { 12 | 13 | @Value(value = "${rest.client.readTimeoutMs:2000}") 14 | private int readTimeout; 15 | 16 | @Value(value = "${rest.client.connectTimeoutMs:2000}") 17 | private int connectTimeout; 18 | 19 | @Bean 20 | public RestTemplate restTemplate() { 21 | return new RestTemplate(createFactory()); 22 | } 23 | 24 | private ClientHttpRequestFactory createFactory() { 25 | HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); 26 | factory.setReadTimeout(readTimeout); 27 | factory.setConnectTimeout(connectTimeout); 28 | return factory; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/internal/config/VelocityPublicFieldUberspect.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2004 The Apache Software Foundation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * Originally PublicFieldUberspect 17 | */ 18 | package org.dogepool.practicalrx.internal.config; 19 | 20 | import java.lang.reflect.Array; 21 | import java.lang.reflect.Field; 22 | 23 | import org.apache.velocity.util.introspection.Info; 24 | import org.apache.velocity.util.introspection.UberspectImpl; 25 | import org.apache.velocity.util.introspection.VelPropertyGet; 26 | import org.apache.velocity.util.introspection.VelPropertySet; 27 | 28 | /** 29 | * Uberspect implementation that exposes public fields. 30 | * Also exposes the explicit "length" field of arrays. 31 | * 32 | *

To use, tell Velocity to use this class for introspection 33 | * by adding the following to your velocity.properties:
34 | * 35 | * 36 | * runtime.introspector.uberspect = org.apache.velocity.tools.generic.introspection.PublicFieldUberspect 37 | * 38 | *

39 | * 40 | * @author Shinobu Kawai 41 | * @version $Id: $ 42 | */ 43 | public class VelocityPublicFieldUberspect extends UberspectImpl { 44 | 45 | /** 46 | * Default constructor. 47 | */ 48 | public VelocityPublicFieldUberspect() 49 | { 50 | } 51 | 52 | /** 53 | * Property getter - returns VelPropertyGet appropos for #set($foo = $bar.woogie). 54 | *
55 | * Returns a special {@link VelPropertyGet} for the length property of arrays. 56 | * Otherwise tries the regular routine. If a getter was not found, 57 | * returns a {@link VelPropertyGet} that gets from public fields. 58 | * 59 | * @param obj the object 60 | * @param identifier the name of the property 61 | * @param i a bunch of information. 62 | * @return a valid VelPropertyGet, if it was found. 63 | * @throws Exception failed to create a valid VelPropertyGet. 64 | */ 65 | public VelPropertyGet getPropertyGet(Object obj, String identifier, Info i) 66 | throws Exception 67 | { 68 | Class clazz = obj.getClass(); 69 | boolean isArray = clazz.isArray(); 70 | boolean isLength = identifier.equals("length"); 71 | if (isArray && isLength) 72 | { 73 | return new ArrayLengthGetter(); 74 | } 75 | 76 | VelPropertyGet getter = super.getPropertyGet(obj, identifier, i); 77 | // there is no clean way to see if super succeeded 78 | // @see http://issues.apache.org/bugzilla/show_bug.cgi?id=31742 79 | try 80 | { 81 | getter.getMethodName(); 82 | return getter; 83 | } 84 | catch (NullPointerException notFound) 85 | { 86 | } 87 | 88 | Field field = obj.getClass().getField(identifier); 89 | if (field != null) 90 | { 91 | return new PublicFieldGetter(field); 92 | } 93 | 94 | return null; 95 | } 96 | 97 | /** 98 | * Property setter - returns VelPropertySet appropos for #set($foo.bar = "geir"). 99 | *
100 | * First tries the regular routine. If a setter was not found, 101 | * returns a {@link VelPropertySet} that sets to public fields. 102 | * 103 | * @param obj the object 104 | * @param identifier the name of the property 105 | * @param arg the value to set to the property 106 | * @param i a bunch of information. 107 | * @return a valid VelPropertySet, if it was found. 108 | * @throws Exception failed to create a valid VelPropertySet. 109 | */ 110 | public VelPropertySet getPropertySet(Object obj, String identifier, 111 | Object arg, Info i) throws Exception 112 | { 113 | VelPropertySet setter = super.getPropertySet(obj, identifier, arg, i); 114 | if (setter != null) 115 | { 116 | return setter; 117 | } 118 | 119 | Field field = obj.getClass().getField(identifier); 120 | if (field != null) 121 | { 122 | return new PublicFieldSetter(field); 123 | } 124 | 125 | return null; 126 | } 127 | 128 | /** 129 | * Implementation of {@link VelPropertyGet} that gets from public fields. 130 | * 131 | * @author Shinobu Kawai 132 | * @version $Id: $ 133 | */ 134 | protected class PublicFieldGetter implements VelPropertyGet 135 | { 136 | /** The Field object representing the property. */ 137 | private Field field = null; 138 | 139 | /** 140 | * Constructor. 141 | * 142 | * @param field The Field object representing the property. 143 | */ 144 | public PublicFieldGetter(Field field) 145 | { 146 | this.field = field; 147 | } 148 | 149 | /** 150 | * Returns the value of the public field. 151 | * 152 | * @param o the object 153 | * @return the value 154 | * @throws Exception failed to get the value from the object 155 | */ 156 | public Object invoke(Object o) throws Exception 157 | { 158 | return this.field.get(o); 159 | } 160 | 161 | /** 162 | * This class is cacheable, so it returns true. 163 | * 164 | * @return true. 165 | */ 166 | public boolean isCacheable() 167 | { 168 | return true; 169 | } 170 | 171 | /** 172 | * Returns "public field getter", since there is no method. 173 | * 174 | * @return "public field getter" 175 | */ 176 | public String getMethodName() 177 | { 178 | return "public field getter"; 179 | } 180 | } 181 | 182 | /** 183 | * Implementation of {@link VelPropertyGet} that gets length from arrays. 184 | * 185 | * @author Shinobu Kawai 186 | * @version $Id: $ 187 | */ 188 | protected class ArrayLengthGetter implements VelPropertyGet 189 | { 190 | /** 191 | * Constructor. 192 | */ 193 | public ArrayLengthGetter() 194 | { 195 | } 196 | 197 | /** 198 | * Returns the length of the array. 199 | * 200 | * @param o the array 201 | * @return the length 202 | * @throws Exception failed to get the length from the array 203 | */ 204 | public Object invoke(Object o) throws Exception 205 | { 206 | // Thanks to Eric Fixler for this refactor. 207 | return new Integer(Array.getLength(o)); 208 | } 209 | 210 | /** 211 | * This class is cacheable, so it returns true. 212 | * 213 | * @return true. 214 | */ 215 | public boolean isCacheable() 216 | { 217 | return true; 218 | } 219 | 220 | /** 221 | * Returns "array length getter", since there is no method. 222 | * 223 | * @return "array length getter" 224 | */ 225 | public String getMethodName() 226 | { 227 | return "array length getter"; 228 | } 229 | } 230 | 231 | /** 232 | * Implementation of {@link VelPropertySet} that sets to public fields. 233 | * 234 | * @author Shinobu Kawai 235 | * @version $Id: $ 236 | */ 237 | protected class PublicFieldSetter implements VelPropertySet 238 | { 239 | /** The Field object representing the property. */ 240 | private Field field = null; 241 | 242 | /** 243 | * Constructor. 244 | * 245 | * @param field The Field object representing the property. 246 | */ 247 | public PublicFieldSetter(Field field) 248 | { 249 | this.field = field; 250 | } 251 | 252 | /** 253 | * Sets the value to the public field. 254 | * 255 | * @param o the object 256 | * @param value the value to set 257 | * @return always null 258 | * @throws Exception failed to set the value to the object 259 | */ 260 | public Object invoke(Object o, Object value) throws Exception 261 | { 262 | this.field.set(o, value); 263 | return null; 264 | } 265 | 266 | /** 267 | * This class is cacheable, so it returns true. 268 | * 269 | * @return true. 270 | */ 271 | public boolean isCacheable() 272 | { 273 | return true; 274 | } 275 | 276 | /** 277 | * Returns "public field setter", since there is no method. 278 | * 279 | * @return "public field setter" 280 | */ 281 | public String getMethodName() 282 | { 283 | return "public field setter"; 284 | } 285 | } 286 | 287 | } 288 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/services/AdminService.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.services; 2 | 3 | import java.math.BigInteger; 4 | import java.time.LocalDate; 5 | import java.time.Month; 6 | 7 | import org.springframework.stereotype.Service; 8 | 9 | /** 10 | * Service for administrative purpose like tracking operational costs. 11 | */ 12 | @Service 13 | public class AdminService { 14 | 15 | public BigInteger costForMonth(int year, Month month) { 16 | LocalDate now = LocalDate.now(); 17 | 18 | if (year == now.getYear() && month == now.getMonth()) { 19 | return BigInteger.ZERO; 20 | } 21 | if (year > now.getYear() 22 | || year == now.getYear() && month.getValue() > now.getMonthValue()) { 23 | return BigInteger.ZERO; 24 | } 25 | return BigInteger.valueOf( 26 | year + 27 | month.getValue() * 100); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/services/CoinService.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.services; 2 | 3 | import org.dogepool.practicalrx.domain.User; 4 | import org.springframework.stereotype.Service; 5 | 6 | /** 7 | * Service for getting info on coins mined by users. 8 | */ 9 | @Service 10 | public class CoinService { 11 | 12 | public long totalCoinsMinedBy(User user) { 13 | if (user.equals(User.OTHERUSER)) { 14 | return 12L; 15 | } else { 16 | return 0L; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/services/ExchangeRateService.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.services; 2 | 3 | import java.util.Map; 4 | 5 | import org.dogepool.practicalrx.error.DogePoolException; 6 | import org.dogepool.practicalrx.error.Error; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.web.client.HttpStatusCodeException; 12 | import org.springframework.web.client.RestClientException; 13 | import org.springframework.web.client.RestTemplate; 14 | 15 | /** 16 | * A facade service to get DOGE to USD and DOGE to other currencies exchange rates. 17 | */ 18 | @Service 19 | public class ExchangeRateService { 20 | 21 | @Value("${doge.api.baseUrl}") 22 | private String dogeUrl; 23 | 24 | @Value("${exchange.free.api.baseUrl}") 25 | private String exchangeUrl; 26 | 27 | @Autowired 28 | private RestTemplate restTemplate; 29 | 30 | public Double dogeToCurrencyExchangeRate(String targetCurrencyCode) { 31 | //get the doge-dollar rate 32 | double doge2usd = dogeToDollar(); 33 | 34 | //get the dollar-currency rate 35 | double usd2currency = dollarToCurrency(targetCurrencyCode); 36 | 37 | //compute the result 38 | return doge2usd * usd2currency; 39 | } 40 | 41 | private double dogeToDollar() { 42 | try { 43 | return restTemplate.getForObject(dogeUrl, Double.class); 44 | } catch (RestClientException e) { 45 | throw new DogePoolException("Unable to reach doge rate service at " + dogeUrl, 46 | Error.UNREACHABLE_SERVICE, HttpStatus.REQUEST_TIMEOUT); 47 | } 48 | } 49 | 50 | private double dollarToCurrency(String currencyCode) { 51 | try { 52 | Map result = restTemplate.getForObject(exchangeUrl + "/{from}/{to}", Map.class, 53 | "USD", currencyCode); 54 | Double rate = (Double) result.get("exchangeRate"); 55 | if (rate == null) 56 | rate = (Double) result.get("rate"); 57 | 58 | if (rate == null) { 59 | throw new DogePoolException("Malformed exchange rate", Error.BAD_CURRENCY, HttpStatus.UNPROCESSABLE_ENTITY); 60 | } 61 | return rate; 62 | } catch (HttpStatusCodeException e) { 63 | throw new DogePoolException("Error processing currency in free API : " + e.getResponseBodyAsString(), 64 | Error.BAD_CURRENCY, e.getStatusCode()); 65 | } catch (RestClientException e) { 66 | throw new DogePoolException("Unable to reach currency exchange service at " + exchangeUrl, 67 | Error.UNREACHABLE_SERVICE, HttpStatus.REQUEST_TIMEOUT); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/services/HashrateService.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.services; 2 | 3 | import org.dogepool.practicalrx.domain.User; 4 | import org.springframework.stereotype.Service; 5 | 6 | /** 7 | * Service to retrieve hashrate information of users. 8 | */ 9 | @Service 10 | public class HashrateService { 11 | 12 | /** 13 | * @param user 14 | * @return the last known gigahash/sec hashrate for the given user 15 | */ 16 | public double hashrateFor(User user) { 17 | if (user.equals(User.USER)) { 18 | return 1.234; 19 | } 20 | return user.displayName.length() / 100d; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/services/PoolRateService.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.services; 2 | 3 | import org.dogepool.practicalrx.domain.User; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Service; 6 | 7 | /** 8 | * Service to retrieve the current global hashrate for the pool. 9 | */ 10 | @Service 11 | public class PoolRateService { 12 | 13 | @Autowired 14 | private PoolService poolService; 15 | 16 | @Autowired 17 | private HashrateService hashrateService; 18 | 19 | public double poolGigaHashrate() { 20 | double hashrate = 0d; 21 | for (User u : poolService.miningUsers()) { 22 | double userRate = hashrateService.hashrateFor(u); 23 | hashrate += userRate; 24 | } 25 | return hashrate; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/services/PoolService.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.services; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | import org.dogepool.practicalrx.domain.User; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | 13 | /** 14 | * Service to retrieve information on the current status of the mining pool 15 | */ 16 | @Service 17 | public class PoolService { 18 | 19 | private final Set connectedUsers = new HashSet<>(); 20 | 21 | public String poolName() { 22 | return "Wow Such Pool!"; 23 | } 24 | 25 | public List miningUsers() { 26 | return new ArrayList<>(connectedUsers); 27 | } 28 | 29 | public boolean connectUser(User user) { 30 | connectedUsers.add(user); 31 | System.out.println(user.nickname + " connected"); 32 | return true; 33 | } 34 | 35 | public boolean disconnectUser(User user) { 36 | connectedUsers.remove(user); 37 | System.out.println(user.nickname + " disconnected"); 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/services/RankingService.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.services; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import org.dogepool.practicalrx.domain.User; 8 | import org.dogepool.practicalrx.domain.UserStat; 9 | import org.dogepool.practicalrx.error.DogePoolException; 10 | import org.dogepool.practicalrx.error.Error; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.stereotype.Service; 14 | 15 | /** 16 | * Service to get ladders and find a user's rankings in the pool. 17 | */ 18 | @Service 19 | public class RankingService { 20 | 21 | @Autowired 22 | private StatService statService; 23 | 24 | /** 25 | * Find the user's rank by hashrate in the pool. This is a costly operation. 26 | * @return the rank of the user in terms of hashrate, or throw a {@link DogePoolException} if it couldnt' be established. 27 | */ 28 | public int rankByHashrate(User user) { 29 | List rankedByHashrate = rankByHashrate(); 30 | int rank = 1; 31 | for (UserStat stat : rankedByHashrate) { 32 | if (stat.user.equals(user)) { 33 | return rank; 34 | } 35 | rank++; 36 | } 37 | throw new DogePoolException("Cannot rank " + user.nickname + " by hashrate", Error.RANK_HASH, HttpStatus.NO_CONTENT); 38 | } 39 | 40 | /** 41 | * Find the user's rank by number of coins found. This is a costly operation. 42 | * @return the rank of the user in terms of coins found, or throw a {@link DogePoolException} if it cannot be established. 43 | */ 44 | public int rankByCoins(User user) { 45 | List rankedByCoins = rankByCoins(); 46 | int rank = 1; 47 | for (UserStat stat : rankedByCoins) { 48 | if (stat.user.equals(user)) { 49 | return rank; 50 | } 51 | rank++; 52 | } 53 | throw new DogePoolException("Cannot rank " + user.nickname + " by coins mined", Error.RANK_COIN, HttpStatus.NO_CONTENT); 54 | } 55 | 56 | public List getLadderByHashrate() { 57 | List ranking = rankByHashrate(); 58 | 59 | if (ranking.isEmpty()) { 60 | return Collections.emptyList(); 61 | } 62 | return new ArrayList<>(ranking.subList(0, Math.min(ranking.size(), 10))); 63 | } 64 | 65 | public List getLadderByCoins() { 66 | List ranking = rankByCoins(); 67 | 68 | if (ranking.isEmpty()) { 69 | return Collections.emptyList(); 70 | } 71 | return new ArrayList<>(ranking.subList(0, Math.min(ranking.size(), 10))); 72 | } 73 | 74 | protected List rankByHashrate() { 75 | List result = statService.getAllStats(); 76 | Collections.sort(result, (o1, o2) -> { 77 | double h1 = o1.hashrate; 78 | double h2 = o2.hashrate; 79 | double diff = h2 - h1; 80 | if (diff == 0d) { 81 | return 0; 82 | } else { 83 | return diff > 0d ? 1 : -1; 84 | } 85 | }); 86 | return result; 87 | } 88 | 89 | protected List rankByCoins() { 90 | List result = statService.getAllStats(); 91 | Collections.sort(result, (o1, o2) -> { 92 | long c1 = o1.totalCoinsMined; 93 | long c2 = o2.totalCoinsMined; 94 | long diff = c2 - c1; 95 | if (diff == 0L) { 96 | return 0; 97 | } else { 98 | return diff > 0L ? 1 : -1; 99 | } 100 | }); 101 | return result; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/services/SearchService.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.services; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.dogepool.practicalrx.domain.User; 7 | import org.dogepool.practicalrx.domain.UserStat; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | /** 12 | * A service to search for users by name, login and other criteria. 13 | */ 14 | @Service 15 | public class SearchService { 16 | 17 | @Autowired 18 | private UserService userService; 19 | 20 | @Autowired 21 | private CoinService coinService; 22 | 23 | /** 24 | * Find users whose display name contain the given namePattern. 25 | * 26 | * @param namePattern the string to find in a user's displayName for him to match (ignoring case). 27 | * @return the list of matching users. 28 | */ 29 | public List findByName(String namePattern) { 30 | String upperPattern = namePattern.toUpperCase(); 31 | List result = new ArrayList<>(); 32 | for (User u : userService.findAll()) { 33 | if (u.displayName.toUpperCase().contains(upperPattern)) { 34 | result.add(u); 35 | } 36 | } 37 | return result; 38 | } 39 | 40 | /** 41 | * Find users according to their number of coins found. 42 | * 43 | * @param minCoins the minimum number of coins found by a user for it to match. 44 | * @param maxCoins the maximum number of coins above which a user won't be considered a match. -1 to ignore. 45 | * @return the list of matching users. 46 | */ 47 | public List findByCoins(long minCoins, long maxCoins) { 48 | 49 | List allUsers = userService.findAll(); 50 | int userListSize = allUsers.size(); 51 | List result = new ArrayList<>(userListSize); 52 | for (User user : allUsers) { 53 | long coins = coinService.totalCoinsMinedBy(user); 54 | if (coins >= minCoins && (maxCoins < 0 || coins <= maxCoins)) { 55 | result.add(new UserStat(user, -1d, coins)); 56 | } 57 | } 58 | return result; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/services/StatService.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.services; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.temporal.ChronoUnit; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Random; 8 | 9 | import org.dogepool.practicalrx.domain.User; 10 | import org.dogepool.practicalrx.domain.UserStat; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Service; 13 | 14 | /** 15 | * Service to get stats on the pool, like top 10 ladders for various criteria. 16 | */ 17 | @Service 18 | public class StatService { 19 | 20 | @Autowired 21 | private HashrateService hashrateService; 22 | 23 | @Autowired 24 | private CoinService coinService; 25 | 26 | @Autowired 27 | private UserService userService; 28 | 29 | public List getAllStats() { 30 | List allUsers = userService.findAll(); 31 | int userListSize = allUsers.size(); 32 | List result = new ArrayList<>(userListSize); 33 | for (User user : allUsers) { 34 | double hashRateForUser = hashrateService.hashrateFor(user); 35 | long coins = coinService.totalCoinsMinedBy(user); 36 | UserStat userStat = new UserStat(user, hashRateForUser, coins); 37 | result.add(userStat); 38 | } 39 | return result; 40 | } 41 | 42 | public LocalDateTime lastBlockFoundDate() { 43 | Random rng = new Random(System.currentTimeMillis()); 44 | return LocalDateTime.now().minus(rng.nextInt(72), ChronoUnit.HOURS); 45 | } 46 | 47 | public User lastBlockFoundBy() { 48 | Random rng = new Random(System.currentTimeMillis()); 49 | int potentiallyBadIndex = rng.nextInt(10); 50 | System.out.println("ELECTED: #" + potentiallyBadIndex); 51 | 52 | List allUsers = userService.findAll(); 53 | return allUsers.get(potentiallyBadIndex); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/services/UserService.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.services; 2 | 3 | import static com.couchbase.client.java.query.dsl.Expression.s; 4 | import static com.couchbase.client.java.query.dsl.Expression.x; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import com.couchbase.client.java.Bucket; 11 | import com.couchbase.client.java.query.N1qlQueryResult; 12 | import com.couchbase.client.java.query.N1qlQueryRow; 13 | import com.couchbase.client.java.query.Select; 14 | import com.couchbase.client.java.query.Statement; 15 | import org.dogepool.practicalrx.domain.User; 16 | import org.dogepool.practicalrx.error.DogePoolException; 17 | import org.dogepool.practicalrx.error.Error; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.beans.factory.annotation.Value; 20 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 21 | import org.springframework.http.HttpStatus; 22 | import org.springframework.stereotype.Service; 23 | 24 | /** 25 | * Service to get user information. 26 | */ 27 | @Service 28 | public class UserService { 29 | 30 | @Autowired(required = false) 31 | private Bucket couchbaseBucket; 32 | 33 | @Value("${store.enableFindAll:false}") 34 | private boolean useCouchbaseForFindAll; 35 | 36 | public User getUser(long id) { 37 | for (User user : findAll()) { 38 | if (user.id == id) { 39 | return user; 40 | } 41 | } 42 | 43 | return null; //TODO any better way of doing this in Java 8? 44 | } 45 | 46 | public User getUserByLogin(String login) { 47 | for (User user : findAll()) { 48 | if (login.equals(user.nickname)) { 49 | return user; 50 | } 51 | } 52 | 53 | return null; //TODO any better way of doing this in Java 8? 54 | } 55 | 56 | public List findAll() { 57 | if (useCouchbaseForFindAll && couchbaseBucket != null) { 58 | try { 59 | Statement statement = Select.select("avatarId", "bio", "displayName", "id", "nickname").from(x("default")) 60 | .where(x("type").eq(s("user"))).groupBy(x("displayName")); 61 | N1qlQueryResult queryResult = couchbaseBucket.query(statement); 62 | List users = new ArrayList(); 63 | for (N1qlQueryRow qr : queryResult) { 64 | users.add(User.fromJsonObject(qr.value())); 65 | } 66 | return users; 67 | } catch (Exception e) { 68 | throw new DogePoolException("Error while getting list of users from database", 69 | Error.DATABASE, HttpStatus.INTERNAL_SERVER_ERROR, e); 70 | } 71 | } else { 72 | return Arrays.asList(User.USER, User.OTHERUSER); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/views/models/IndexModel.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.views.models; 2 | 3 | import java.util.List; 4 | 5 | import org.dogepool.practicalrx.domain.UserStat; 6 | 7 | public class IndexModel { 8 | 9 | private List hashLadder; 10 | private List coinsLadder; 11 | private String poolName; 12 | private int miningUserCount; 13 | private Double gigaHashrate; 14 | private String dogeToUsdMessage; 15 | private String dogeToEurMessage; 16 | 17 | public List getHashLadder() { 18 | return hashLadder; 19 | } 20 | 21 | public void setHashLadder(List hashLadder) { 22 | this.hashLadder = hashLadder; 23 | } 24 | 25 | public List getCoinsLadder() { 26 | return coinsLadder; 27 | } 28 | 29 | public void setCoinsLadder(List coinsLadder) { 30 | this.coinsLadder = coinsLadder; 31 | } 32 | 33 | public String getPoolName() { 34 | return poolName; 35 | } 36 | 37 | public void setPoolName(String poolName) { 38 | this.poolName = poolName; 39 | } 40 | 41 | public int getMiningUserCount() { 42 | return miningUserCount; 43 | } 44 | 45 | public void setMiningUserCount(int miningUserCount) { 46 | this.miningUserCount = miningUserCount; 47 | } 48 | 49 | public Double getGigaHashrate() { 50 | return gigaHashrate; 51 | } 52 | 53 | public void setGigaHashrate(Double gigaHashrate) { 54 | this.gigaHashrate = gigaHashrate; 55 | } 56 | 57 | public String getDogeToUsdMessage() { 58 | return dogeToUsdMessage; 59 | } 60 | 61 | public void setDogeToUsdMessage(String dogeToUsdMessage) { 62 | this.dogeToUsdMessage = dogeToUsdMessage; 63 | } 64 | 65 | public String getDogeToEurMessage() { 66 | return dogeToEurMessage; 67 | } 68 | 69 | public void setDogeToEurMessage(String dogeToEurMessage) { 70 | this.dogeToEurMessage = dogeToEurMessage; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/org/dogepool/practicalrx/views/models/MinerModel.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.views.models; 2 | 3 | public class MinerModel { 4 | 5 | private String smallAvatarUrl; 6 | private String displayName; 7 | private String nickname; 8 | private String bio; 9 | private String avatarUrl; 10 | private long rankByCoins; 11 | private long rankByHash; 12 | 13 | public String getSmallAvatarUrl() { 14 | return smallAvatarUrl; 15 | } 16 | 17 | public void setSmallAvatarUrl(String smallAvatarUrl) { 18 | this.smallAvatarUrl = smallAvatarUrl; 19 | } 20 | 21 | public String getDisplayName() { 22 | return displayName; 23 | } 24 | 25 | public void setDisplayName(String displayName) { 26 | this.displayName = displayName; 27 | } 28 | 29 | public String getNickname() { 30 | return nickname; 31 | } 32 | 33 | public void setNickname(String nickname) { 34 | this.nickname = nickname; 35 | } 36 | 37 | public String getBio() { 38 | return bio; 39 | } 40 | 41 | public void setBio(String bio) { 42 | this.bio = bio; 43 | } 44 | 45 | public String getAvatarUrl() { 46 | return avatarUrl; 47 | } 48 | 49 | public void setAvatarUrl(String avatarUrl) { 50 | this.avatarUrl = avatarUrl; 51 | } 52 | 53 | public long getRankByCoins() { 54 | return rankByCoins; 55 | } 56 | 57 | public void setRankByCoins(long rankByCoins) { 58 | this.rankByCoins = rankByCoins; 59 | } 60 | 61 | public long getRankByHash() { 62 | return rankByHash; 63 | } 64 | 65 | public void setRankByHash(long rankByHash) { 66 | this.rankByHash = rankByHash; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/resources/static/images/dogeError.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbasle/practicalRx/f61f6530b515c27f5574af60546a74594ec327cb/src/main/resources/static/images/dogeError.jpg -------------------------------------------------------------------------------- /src/main/resources/templates/error.vm: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | WOW such error! 7 | 8 | 9 | 10 |

Something went wrong:

11 |

${status} ${error} -

12 |

13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/templates/errorWithDetail.vm: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | WOW such error! 7 | 8 | 9 | 10 |

Something went wrong:

11 |

$msg

12 |

13 | Much Details! 14 |
    15 |
  • Error Code: $errorCode
  • 16 |
  • Error Category: $errorCategory
  • 17 |
  • Http Status: $httpStatus
  • 18 |
19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/templates/index.vm: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Welcome to $model.poolName dogecoin mining pool 7 | 8 | 9 | 10 | 11 |
12 | 13 |

Welcome to $model.poolName dogecoin mining pool

14 |

$model.miningUserCount users currently mining, for a global hashrate of $model.gigaHashrate GHash/s

15 |

16 | $model.dogeToUsdMessage 17 |
$model.dogeToEurMessage 18 |

19 |

20 |

21 |

TOP 10 Miners by Hashrate

22 |
23 | #foreach( $userStat in $model.hashLadder ) 24 |
25 |
26 | $userStat.user.nickname 27 |
$userStat.hashrate GHash/s
28 |
29 |
30 | #end 31 |
32 |
33 |

TOP 10 Miners by Coins Found

34 |
35 | #foreach($userStat in $model.coinsLadder) 36 |
37 |
38 | $userStat.user.nickname 39 |
$userStat.totalCoinsMined dogecoins
40 |
41 |
42 | #end 43 |
44 |
45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/resources/templates/miner.vm: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Miner $minerModel.displayName 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 |
17 |
$minerModel.nickname
18 | $minerModel.displayName 19 |
20 |
21 |

$minerModel.bio

22 |
23 |
24 |
25 | 28 | 29 | Rank by Hashrate: $minerModel.rankByHash 30 | 31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /src/test/java/org/dogepool/practicalrx/controllers/AdminControllerTest.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import static org.hamcrest.Matchers.contains; 4 | import static org.hamcrest.Matchers.containsString; 5 | import static org.hamcrest.Matchers.startsWith; 6 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 7 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 11 | 12 | import org.dogepool.practicalrx.Main; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.boot.test.SpringApplicationConfiguration; 18 | import org.springframework.http.MediaType; 19 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 20 | import org.springframework.test.context.web.WebAppConfiguration; 21 | import org.springframework.test.web.servlet.MockMvc; 22 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 23 | import org.springframework.web.context.WebApplicationContext; 24 | 25 | @RunWith(SpringJUnit4ClassRunner.class) 26 | @SpringApplicationConfiguration(classes = Main.class) 27 | @WebAppConfiguration 28 | public class AdminControllerTest { 29 | 30 | @Autowired 31 | private WebApplicationContext webApplicationContext; 32 | 33 | private MockMvc mockMvc; 34 | 35 | @Before 36 | public void setup() throws Exception { 37 | this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 38 | } 39 | 40 | 41 | @Test 42 | public void testRegisterMiningBadUser() throws Exception { 43 | mockMvc.perform(post("/admin/mining/1024")) 44 | .andExpect(status().isNotFound()); 45 | } 46 | 47 | @Test 48 | public void testRegisterMiningGoodUser() throws Exception { 49 | mockMvc.perform(post("/admin/mining/1")) 50 | .andExpect(status().isAccepted()) 51 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 52 | .andExpect(content().string(startsWith("["))); 53 | } 54 | 55 | @Test 56 | public void testDeregisterMiningBadUser() throws Exception { 57 | mockMvc.perform(delete("/admin/mining/1024")) 58 | .andExpect(status().isNotFound()); 59 | } 60 | 61 | @Test 62 | public void testDeregisterMiningGoodUser() throws Exception { 63 | mockMvc.perform(delete("/admin/mining/1")) 64 | .andExpect(status().isAccepted()) 65 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 66 | .andExpect(content().string(startsWith("["))); 67 | } 68 | 69 | @Test 70 | public void testCost() throws Exception { 71 | mockMvc.perform(get("/admin/cost/")) 72 | .andExpect(status().isOk()) 73 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 74 | .andExpect(content().string(containsString("\"cost\""))) 75 | .andExpect(content().string(containsString("\"month\""))) 76 | .andExpect(content().string(containsString("\"currency\""))) 77 | .andExpect(content().string(containsString("\"currencySign\""))); 78 | 79 | } 80 | 81 | @Test 82 | public void testCostMonthName() throws Exception { 83 | mockMvc.perform(get("/admin/cost/2015/JANUARY")) 84 | .andExpect(status().isOk()) 85 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 86 | .andExpect(content().json("{\"cost\":2115,\"month\":\"JANUARY 2015\"," + 87 | "\"currencySign\":\"$\",\"currency\":\"USD\"}")); 88 | } 89 | 90 | @Test 91 | public void testCostMonthBadName() throws Exception { 92 | mockMvc.perform(get("/admin/cost/2015/FOO")) 93 | .andExpect(status().isBadRequest()); 94 | } 95 | 96 | @Test 97 | public void testCostMonthNumber() throws Exception { 98 | mockMvc.perform(get("/admin/cost/2015-01")) 99 | .andExpect(status().isOk()) 100 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 101 | .andExpect(content().json("{\"cost\":2115,\"month\":\"JANUARY 2015\"," + 102 | "\"currencySign\":\"$\",\"currency\":\"USD\"}")); 103 | } 104 | } -------------------------------------------------------------------------------- /src/test/java/org/dogepool/practicalrx/controllers/IndexControllerTest.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 4 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 7 | 8 | import org.dogepool.practicalrx.Main; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.SpringApplicationConfiguration; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 16 | import org.springframework.test.context.web.WebAppConfiguration; 17 | import org.springframework.test.web.servlet.MockMvc; 18 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 19 | import org.springframework.web.context.WebApplicationContext; 20 | 21 | @RunWith(SpringJUnit4ClassRunner.class) 22 | @SpringApplicationConfiguration(classes = Main.class) 23 | @WebAppConfiguration 24 | public class IndexControllerTest { 25 | 26 | @Autowired 27 | private WebApplicationContext webApplicationContext; 28 | 29 | private MockMvc mockMvc; 30 | 31 | @Before 32 | public void setup() throws Exception { 33 | this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 34 | } 35 | 36 | 37 | @Test 38 | public void testIndex() throws Exception { 39 | mockMvc.perform(get("/")) 40 | .andExpect(status().isOk()) 41 | .andExpect(model().attributeHasNoErrors("model")) 42 | .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)); 43 | } 44 | } -------------------------------------------------------------------------------- /src/test/java/org/dogepool/practicalrx/controllers/PoolControllerTest.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import static org.hamcrest.Matchers.containsString; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 7 | 8 | import org.dogepool.practicalrx.Main; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.SpringApplicationConfiguration; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 16 | import org.springframework.test.context.web.WebAppConfiguration; 17 | import org.springframework.test.web.servlet.MockMvc; 18 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 19 | import org.springframework.web.context.WebApplicationContext; 20 | 21 | @RunWith(SpringJUnit4ClassRunner.class) 22 | @SpringApplicationConfiguration(classes = Main.class) 23 | @WebAppConfiguration 24 | public class PoolControllerTest { 25 | 26 | @Autowired 27 | private WebApplicationContext webApplicationContext; 28 | 29 | private MockMvc mockMvc; 30 | 31 | @Before 32 | public void setup() throws Exception { 33 | this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 34 | } 35 | 36 | @Test 37 | public void testLadderByHashrate() throws Exception { 38 | String expected = "[{\"user\":{\"id\":0,\"nickname\":\"user0\",\"displayName\":\"Test User\"," + 39 | "\"bio\":\"Story of my life.\\nEnd of Story.\",\"avatarId\":\"12434\",\"type\":\"user\"}," + 40 | "\"hashrate\":1.234,\"totalCoinsMined\":0}," + 41 | "{\"user\":{\"id\":1,\"nickname\":\"richUser\",\"displayName\":\"Richie Rich\"," + 42 | "\"bio\":\"I'm rich I have dogecoin\",\"avatarId\":\"45678\",\"type\":\"user\"}," + 43 | "\"hashrate\":0.11,\"totalCoinsMined\":12}]"; 44 | System.out.println(expected); 45 | 46 | mockMvc.perform(get("/pool/ladder/hashrate")) 47 | .andExpect(status().isOk()) 48 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 49 | .andExpect(content().json(expected)); 50 | } 51 | 52 | @Test 53 | public void testLadderByCoins() throws Exception { 54 | String expected = "[{\"user\":{\"id\":1,\"nickname\":\"richUser\",\"displayName\":\"Richie Rich\"," + 55 | "\"bio\":\"I'm rich I have dogecoin\",\"avatarId\":\"45678\",\"type\":\"user\"},\"hashrate\":0.11," + 56 | "\"totalCoinsMined\":12},{\"user\":{\"id\":0,\"nickname\":\"user0\",\"displayName\":\"Test User\"," + 57 | "\"bio\":\"Story of my life.\\nEnd of Story.\"," + 58 | "\"avatarId\":\"12434\",\"type\":\"user\"},\"hashrate\":1.234,\"totalCoinsMined\":0}]"; 59 | System.out.println(expected); 60 | 61 | mockMvc.perform(get("/pool/ladder/coins")) 62 | .andExpect(status().isOk()) 63 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 64 | .andExpect(content().json(expected)); 65 | } 66 | 67 | @Test 68 | public void testGlobalHashRate() throws Exception { 69 | String expected = "{\"hashrate\":1.234,\"unit\":\"GHash/s\"}"; 70 | 71 | mockMvc.perform(get("/pool/hashrate")) 72 | .andExpect(status().isOk()) 73 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 74 | .andExpect(content().json(expected)); 75 | } 76 | 77 | @Test 78 | public void testMiners() throws Exception { 79 | String expected = "{\"totalMiningUsers\":1,\"totalUsers\":2}"; 80 | 81 | mockMvc.perform(get("/pool/miners")) 82 | .andExpect(status().isOk()) 83 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 84 | .andExpect(content().json(expected)); 85 | } 86 | 87 | @Test 88 | public void testActiveMiners() throws Exception { 89 | String expected = "[{\"id\":0,\"nickname\":\"user0\",\"displayName\":\"Test User\"," + 90 | "\"bio\":\"Story of my life.\\nEnd of Story.\",\"avatarId\":\"12434\",\"type\":\"user\"}]"; 91 | System.out.println(expected); 92 | 93 | mockMvc.perform(get("/pool/miners/active")) 94 | .andExpect(status().isOk()) 95 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 96 | .andExpect(content().json(expected)); 97 | } 98 | 99 | @Test 100 | public void testLastBlock() throws Exception { 101 | //there's some part of randomness in this one 102 | mockMvc.perform(get("/pool/lastblock")) 103 | .andExpect(status().isOk()) 104 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 105 | .andExpect(content().string(containsString("\"foundAgo\""))) 106 | .andExpect(content().string(containsString("\"foundBy\""))) 107 | .andExpect(content().string(containsString("\"foundOn\""))); 108 | } 109 | } -------------------------------------------------------------------------------- /src/test/java/org/dogepool/practicalrx/controllers/RateControllerTest.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import static org.hamcrest.Matchers.containsString; 4 | import static org.hamcrest.Matchers.startsWith; 5 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 7 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 8 | 9 | import org.dogepool.practicalrx.Main; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.SpringApplicationConfiguration; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 17 | import org.springframework.test.context.web.WebAppConfiguration; 18 | import org.springframework.test.web.servlet.MockMvc; 19 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 20 | import org.springframework.web.context.WebApplicationContext; 21 | 22 | @RunWith(SpringJUnit4ClassRunner.class) 23 | @SpringApplicationConfiguration(classes = Main.class) 24 | @WebAppConfiguration 25 | public class RateControllerTest { 26 | 27 | @Autowired 28 | private WebApplicationContext webApplicationContext; 29 | 30 | private MockMvc mockMvc; 31 | 32 | @Before 33 | public void setup() throws Exception { 34 | this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 35 | } 36 | 37 | @Test 38 | public void testRateEuro() throws Exception { 39 | mockMvc.perform(get("/rate/EUR").accept(MediaType.APPLICATION_JSON)) 40 | .andExpect(status().isOk()) 41 | .andExpect(content().string(startsWith("{\"moneyCodeFrom\":\"DOGE\",\"moneyCodeTo\":\"EUR\",\"exchangeRate\":"))); 42 | } 43 | 44 | @Test 45 | public void testRateBadCurrencyTooLong() throws Exception { 46 | //Note: the configuration of test has a timeout of 6 seconds, which will always succeed 47 | mockMvc.perform(get("/rate/EURO").accept(MediaType.APPLICATION_JSON)) 48 | .andExpect(status().isNotFound()) 49 | .andExpect(content().string(containsString("Unknown currency EURO"))); 50 | } 51 | 52 | @Test 53 | public void testRateBadCurrencyBadCase() throws Exception { 54 | //Note: the configuration of test has a timeout of 6 seconds, which will always succeed 55 | mockMvc.perform(get("/rate/EuR").accept(MediaType.APPLICATION_JSON)) 56 | .andExpect(status().isNotFound()) 57 | .andExpect(content().string(containsString("Unknown currency EuR"))); 58 | } 59 | } -------------------------------------------------------------------------------- /src/test/java/org/dogepool/practicalrx/controllers/SearchControllerTest.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 4 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 6 | 7 | import java.util.List; 8 | 9 | import org.dogepool.practicalrx.Main; 10 | import org.dogepool.practicalrx.domain.User; 11 | import org.dogepool.practicalrx.domain.UserStat; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.SpringApplicationConfiguration; 17 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 18 | import org.springframework.test.context.web.WebAppConfiguration; 19 | import org.springframework.test.web.servlet.MockMvc; 20 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 21 | import org.springframework.web.bind.annotation.PathVariable; 22 | import org.springframework.web.bind.annotation.RequestMapping; 23 | import org.springframework.web.context.WebApplicationContext; 24 | 25 | @RunWith(SpringJUnit4ClassRunner.class) 26 | @SpringApplicationConfiguration(classes = Main.class) 27 | @WebAppConfiguration 28 | public class SearchControllerTest { 29 | 30 | @Autowired 31 | private WebApplicationContext webApplicationContext; 32 | 33 | private MockMvc mockMvc; 34 | 35 | @Before 36 | public void setup() throws Exception { 37 | this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 38 | } 39 | 40 | @Test 41 | public void testSearchByName() throws Exception { 42 | String expected = "[{\"id\":1,\"nickname\":\"richUser\",\"displayName\":\"Richie Rich\"," + 43 | "\"bio\":\"I'm rich I have dogecoin\",\"avatarId\":\"45678\",\"type\":\"user\"}]"; 44 | 45 | mockMvc.perform(get("/search/user/{pattern}", "richie")) 46 | .andExpect(status().isOk()) 47 | .andExpect(content().json(expected)); 48 | } 49 | 50 | @Test 51 | public void testSearchByNameNoResult() throws Exception { 52 | mockMvc.perform(get("/search/user/{pattern}", "alfred")) 53 | .andExpect(status().isOk()) 54 | .andExpect(content().json("[]")); 55 | } 56 | 57 | @Test 58 | public void testSearchByCoinsMinOnly() throws Exception { 59 | String expected = "[{\"user\":{\"id\":1,\"nickname\":\"richUser\",\"displayName\":\"Richie Rich\"," + 60 | "\"bio\":\"I'm rich I have dogecoin\",\"avatarId\":\"45678\",\"type\":\"user\"}," + 61 | "\"hashrate\":-1.0,\"totalCoinsMined\":12}]"; 62 | 63 | mockMvc.perform(get("/search/user/coins/{min}", 10)) 64 | .andExpect(status().isOk()) 65 | .andExpect(content().json(expected)); 66 | } 67 | 68 | @Test 69 | public void testSearchByCoinsMinNoMatch() throws Exception { 70 | mockMvc.perform(get("/search/user/coins/{min}", 1200)) 71 | .andExpect(status().isOk()) 72 | .andExpect(content().json("[]")); 73 | } 74 | 75 | @Test 76 | public void testSearchByCoinsMinMax() throws Exception { 77 | String expected = "[{\"user\":{\"id\":1,\"nickname\":\"richUser\",\"displayName\":\"Richie Rich\"," + 78 | "\"bio\":\"I'm rich I have dogecoin\",\"avatarId\":\"45678\",\"type\":\"user\"}," + 79 | "\"hashrate\":-1.0,\"totalCoinsMined\":12}]"; 80 | 81 | mockMvc.perform(get("/search/user/coins/{min}/{max}", 10, 13)) 82 | .andExpect(status().isOk()) 83 | .andExpect(content().json(expected)); 84 | } 85 | 86 | @Test 87 | public void testSearchByCoinsMinMaxNoMinMatch() throws Exception { 88 | mockMvc.perform(get("/search/user/coins/{min}/{max}", 13, 14)) 89 | .andExpect(status().isOk()) 90 | .andExpect(content().json("[]")); 91 | } 92 | 93 | @Test 94 | public void testSearchByCoinsMinMaxNoMaxMatch() throws Exception { 95 | mockMvc.perform(get("/search/user/coins/{min}/{max}", 1, 11)) 96 | .andExpect(status().isOk()) 97 | .andExpect(content().json("[]")); 98 | } 99 | } -------------------------------------------------------------------------------- /src/test/java/org/dogepool/practicalrx/controllers/UserProfileControllerTest.java: -------------------------------------------------------------------------------- 1 | package org.dogepool.practicalrx.controllers; 2 | 3 | import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 7 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; 8 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 10 | 11 | import org.dogepool.practicalrx.Main; 12 | import org.dogepool.practicalrx.error.*; 13 | import org.dogepool.practicalrx.error.Error; 14 | import org.hamcrest.Matchers; 15 | import org.junit.Before; 16 | import org.junit.Test; 17 | import org.junit.runner.RunWith; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.boot.test.SpringApplicationConfiguration; 20 | import org.springframework.http.HttpStatus; 21 | import org.springframework.http.MediaType; 22 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 23 | import org.springframework.test.context.web.WebAppConfiguration; 24 | import org.springframework.test.web.servlet.MockMvc; 25 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 26 | import org.springframework.web.context.WebApplicationContext; 27 | 28 | @RunWith(SpringJUnit4ClassRunner.class) 29 | @SpringApplicationConfiguration(classes = Main.class) 30 | @WebAppConfiguration 31 | public class UserProfileControllerTest { 32 | 33 | @Autowired 34 | private WebApplicationContext webApplicationContext; 35 | 36 | private MockMvc mockMvc; 37 | 38 | @Before 39 | public void setup() throws Exception { 40 | this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 41 | } 42 | 43 | @Test 44 | public void testProfile() throws Exception { 45 | String expected = ""; 46 | 47 | mockMvc.perform(get("/miner/{id}", 1).accept(MediaType.APPLICATION_JSON)) 48 | .andExpect(status().isOk()) 49 | .andExpect(content().string(expected)); 50 | } 51 | 52 | @Test 53 | public void testProfileNotFound() throws Exception { 54 | DogePoolException expected = new DogePoolException("Unknown miner", 55 | org.dogepool.practicalrx.error.Error.UNKNOWN_USER, 56 | HttpStatus.NOT_FOUND); 57 | 58 | mockMvc.perform(get("/miner/{id}", 1000).accept(MediaType.APPLICATION_JSON)) 59 | .andExpect(status().isOk()) 60 | .andExpect(request().asyncStarted()) 61 | .andExpect(request().asyncResult(expected)); 62 | } 63 | } -------------------------------------------------------------------------------- /src/test/resources/testResources.txt: -------------------------------------------------------------------------------- 1 | The src/test/resources folder should contain a copy of application.test that you edited in order for unit tests to run. --------------------------------------------------------------------------------