├── .editorconfig ├── .gitignore ├── Dockerfile ├── Procfile ├── README.adoc ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ ├── App.java │ ├── jooq │ ├── JooqModule.java │ ├── Keys.java │ ├── Public.java │ ├── Sequences.java │ ├── Tables.java │ └── tables │ │ ├── Meeting.java │ │ └── records │ │ └── MeetingRecord.java │ ├── meeting │ ├── DefaultMeetingRepository.java │ ├── DefaultMeetingService.java │ ├── Meeting.java │ ├── MeetingChainAction.java │ ├── MeetingModule.java │ ├── MeetingRepository.java │ └── MeetingService.java │ ├── redis │ ├── DefaultRatingRepository.java │ ├── RatingRepository.java │ ├── RedisConfig.java │ └── RedisModule.java │ └── util │ └── HerokuUtils.java └── ratpack ├── .ratpack ├── postgres.yaml └── redis.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_style = space 4 | indent_size = 2 5 | 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | out 4 | *.ipr 5 | *.iws 6 | *.iml 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | RUN apt-get update && apt-get install -y redis-server 3 | EXPOSE 6379 4 | ENTRYPOINT ["/usr/bin/redis-server"] 5 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: env DATABASE_URL=$DATABASE_URL build/installShadow/modern-java-web/bin/modern-java-web redis.url=$REDIS_URL 2 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Modern Java Web Development 2 | Dan Hyun <@Lspacewalker> 3 | :experimental: 4 | :icons: font 5 | :sectanchors: 6 | :sectlinks: 7 | 8 | == What Is Modern Web Development? 9 | 10 | * Good Developer Experience 11 | ** Fast feedback loop 12 | ** Good testability 13 | ** JustWorks (TM) 14 | * No more WAR! 15 | * http://12factor.net[12 Factor App] 16 | 17 | == Toolbox 18 | 19 | * <> (<>, http://redis.paluch.biz/[Lettuce], http://javaslang.com/[Javaslang], <>) 20 | * <> 21 | * <> 22 | * <> 23 | * <> 24 | 25 | * Twitter! Follow https://twitter.com/Lspacewalker[@Lspacewalker] 26 | 27 | 28 | === Java 8 29 | 30 | * Java 6 EOL 2013-02 31 | * Java 7 EOL 2015-02 32 | * Java 9 2016 Q4 (2017 late Q1? Jigsaw extension) 33 | * `λ -> {}` 34 | * Interface Upgrade 35 | ** Default and static methods 36 | * Stream API 37 | 38 | 39 | === Gradle 40 | 41 | * Gradle wrapper - batteries included (`gradlew`) 42 | * Consistent, reproducible builds 43 | - Avoid "Works On My Machine" syndrome 44 | * Great IDE integration 45 | * Continuous build mode (TDD) 46 | - `./gradlew -t or ./gradlew --continuous` 47 | 48 | ==== Gradle bootstrap 49 | 50 | `gradle init` gives you... 51 | 52 | gradlew and gradlew.bat:: 53 | Gradle wrapper scripts 54 | 55 | build.gradle:: 56 | Main build file, specifies plugins, tasks, dependencies 57 | 58 | settings.gradle:: 59 | Extra project metadata settings 60 | 61 | .Indispensable plugins 62 | 1. `application` 63 | a. Create ops friendly distributions 64 | a. Don't forget to set `mainClassName` 65 | 1. https://github.com/johnrengelman/shadow[Gradle Shadow Plugin] 66 | a. Creates "fat jars", plays nicely with `application` plugin 67 | 1. `idea` 68 | a. Customize IntelliJ project/module/workspace 69 | a. Don't VCS IDE settings 70 | 71 | .New plugins method https://docs.gradle.org/current/dsl/org.gradle.plugin.use.PluginDependenciesSpec.html[(Incubating feature)] 72 | [source, gradle] 73 | ---- 74 | plugins { 75 | id 'java' 76 | id 'idea' 77 | id 'com.github.johnrengelman.shadow' version '1.2.2' 78 | // id "$id" version "$version" 79 | } 80 | ---- 81 | 82 | .Customize IntelliJ Integration 83 | [source, gradle] 84 | ---- 85 | idea { 86 | project { 87 | jdkName = '1.8' // <1> 88 | languageLevel = '1.8' // <2> 89 | vcs = 'Git' // <3> 90 | } 91 | } 92 | ---- 93 | <1> Set JDK to 1.8 94 | <2> Set target language level to 1.8 95 | <3> Set VCS manager to Git 96 | 97 | === Producing artifacts 98 | 99 | `./gradlew shadowJar` 100 | 101 | Produces executable jar with flattened dependencies. 102 | 103 | `./gradlew installShadowApp` 104 | 105 | Produces executable jar and shell scripts for starting jar. Can also produce zip file via `./gradlew distShadowZip` or tar via `./gradlew distShadowTar` 106 | 107 | === EditorConfig 108 | 109 | .What is http://editorconfig.org/#overview[EditorConfig]? 110 | > EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs. 111 | 112 | Don't argue about formatting, pick a standard and stick to it. 113 | 114 | .Sample .editorconfig 115 | [source, python] 116 | ---- 117 | root = true 118 | 119 | [*] # for all files 120 | indent_style = space 121 | indent_size = 2 122 | 123 | # We recommend you to keep these unchanged 124 | end_of_line = lf 125 | charset = utf-8 126 | trim_trailing_whitespace = true 127 | insert_final_newline = true 128 | ---- 129 | 130 | Supported by many IDEs, e.g. IntelliJ kbd:[CTRL + ALT + L] 131 | 132 | === Docker 133 | 134 | * Nice functionality around LXC 135 | * Images, file-system layer snap-shotting 136 | * Lighter than Virtualization 137 | ** Total size 138 | ** Boot Time 139 | * Counters "Works On My Machine" syndrome 140 | * Nice way to bring up services/dependencies/mechanisms that may not be available for your OS 141 | 142 | ==== docker-machine 143 | 144 | * Tool to provision Docker ready VM for Mac/Win 145 | 146 | Once setup, you need to inform your environment about the VM. 147 | 148 | .Ask docker-machine about default's environment config 149 | ``` 150 | $ docker-machine env default 151 | export DOCKER_TLS_VERIFY="1" 152 | export DOCKER_HOST="tcp://192.168.99.100:2376" 153 | export DOCKER_CERT_PATH="C:\Users\danny\.docker\machine\machines\default" 154 | export DOCKER_MACHINE_NAME="default" 155 | # Run this command to configure your shell: 156 | # eval "$(C:\Program Files\Docker Toolbox\docker-machine.exe env default)" 157 | ``` 158 | 159 | ==== Dockerized Redis 160 | 161 | .Dockerfile 162 | [source, docker] 163 | ---- 164 | FROM ubuntu:14.04 # <1> 165 | RUN apt-get update && apt-get install -y redis-server # <2> 166 | EXPOSE 6379 # <3> 167 | ENTRYPOINT ["/usr/bin/redis-server"] # <4> 168 | ---- 169 | <1> Base image from Ubuntu Trusty image 170 | <2> Install Redis into new image 171 | <3> Declare that container is listening on port 6379 172 | <4> Start Redis server when container starts 173 | 174 | http://docs.docker.com/engine/reference/builder/[Dockerfile reference] 175 | 176 | .bash 177 | ``` 178 | $ docker build -t danhyun/redis . 179 | $ docker run --name redis -d -p 6379:6379 danhyun/redis 180 | $ docker exec -it redis bash 181 | 182 | root@d42014247c2e:/# redis-cli 183 | 127.0.0.1:6379> set hello world 184 | OK 185 | 127.0.0.1:6379> get hello 186 | "world" 187 | 127.0.0.1:6379> del hello 188 | (integer) 1 189 | 127.0.0.1:6379> get hello 190 | (nil) 191 | ``` 192 | 193 | ==== Dockerized Postgres 194 | 195 | .Create new PostgreSQL container from existing Dockerfile 196 | ``` 197 | $ docker run --name postgres -e POSTGRES_PASSWORD=password -d -p 5432:5432 postgres 198 | ``` 199 | 200 | This command pulls down a `postgres` Docker image from https://hub.docker.com/_/postgres/[Docker Hub], names the container `postgres`, detaches from session, maps container's port 5432 to local port 5432. 201 | 202 | .Access PostgreSQL from running container's command line 203 | ``` 204 | $ docker exec -it postgres bash 205 | root@b1db931a37a7:/# psql -U postgres 206 | psql (9.4.5) 207 | Type "help" for help. 208 | 209 | postgres=# \l 210 | postgres=# create database modern; 211 | CREATE DATABASE 212 | postgres=# \c modern 213 | You are now connected to database "modern" as user "postgres". 214 | 215 | modern=# create table meeting ( 216 | id serial primary key, 217 | organizer varchar(255), 218 | topic varchar(255), 219 | description text 220 | ); 221 | 222 | CREATE TABLE 223 | 224 | modern=#insert into meeting 225 | (organizer, topic, description) 226 | values 227 | ('Dan H', 'Modern Java Web Development', 'A survey of essential tools/frameworks/techniques for the modern Java developer'); 228 | 229 | INSERT 0 1 230 | 231 | modern=# select * from meeting; 232 | id | organizer | topic | description 233 | ----+-----------+-----------------------------+-------------------------------------------------------------------------------- 234 | 1 | Dan H | Modern Java Web Development | A survey of essentia tools/frameworks/techniques for the modern Java developer 235 | (1 row) 236 | ``` 237 | 238 | === Heroku 239 | 240 | * Free signup 241 | * Rapid prototyping (free versions of services available) 242 | 243 | === Install Heroku Toolbelt 244 | 245 | Get the Heroku toolbelt https://toolbelt.heroku.com/[here] 246 | 247 | === Prepare app for Heroku 248 | 249 | Heroku only needs 2 things: 250 | 251 | 1. `Procfile` - tells Heroku what to execute 252 | 1. A `stage` task from Gradle 253 | 254 | 255 | == Ratpack 256 | 257 | * JDK 8+ 258 | - just jar files, no binaries to install, no codegen 259 | * Minimal framework overhead (low resource usage, save $$$) 260 | * Unopinionated - Make your app solve your problems, don't let framework get in the way 261 | * Reactive, Non-blocking and fully asynchronous 262 | * Excellent testing support 263 | 264 | 265 | === Handlers 266 | 267 | * Functional interface 268 | * `void handle(Context context) {}` 269 | * send response now or delegate to the next handler 270 | 271 | === Chain 272 | 273 | * convenience API for specifying request handling flow 274 | * "if-else" for handlers 275 | * Chains are composable 276 | 277 | === Registry 278 | 279 | * Map like lookup for services 280 | * Immutable 281 | * Way to communicate between handlers 282 | 283 | === Async 284 | 285 | * Promises 286 | * Operations 287 | * Blocking 288 | 289 | == HikariCP 290 | 291 | Blazing fast JDBC library. 292 | 293 | https://github.com/brettwooldridge/HikariCP/wiki/Down-the-Rabbit-Hole[Technical details] 294 | 295 | === Config 296 | 297 | Configure HikariCP to use our dockerized PostgreSQL instance. 298 | 299 | .postgres.yaml 300 | [source, yaml] 301 | ---- 302 | db: 303 | dataSourceClassName: org.postgresql.ds.PGSimpleDataSource 304 | username: postgres 305 | password: password 306 | dataSourceProperties: 307 | databaseName: modern 308 | serverName: 192.168.99.100 309 | portNumber: 5432 310 | ---- 311 | 312 | === Apply Config 313 | 314 | [source, java] 315 | .Configure Hikari DataSource provider 316 | ---- 317 | .module(HikariModule.class, config -> { 318 | config 319 | .setDataSourceClassName("org.postgresql.ds.PGSimpleDataSource"); 320 | config.setUsername("postgres"); 321 | config.setPassword("password"); 322 | config.addDataSourceProperty("databaseName", "modern"); 323 | config.addDataSourceProperty("serverName", "192.168.99.100"); 324 | config.addDataSourceProperty("portNumber", "5432"); 325 | }) 326 | ---- 327 | 328 | 329 | .Use a Config Object 330 | [source, java] 331 | ---- 332 | .bindInstance(HikariConfig.class, configData.get("/db", HikariConfig.class)) 333 | .module(HikariModule.class) 334 | ---- 335 | 336 | .Even better 337 | [source, java] 338 | ---- 339 | ServerConfig configData = ServerConfig.builder() 340 | .baseDir(BaseDir.find()) 341 | .yaml("db.yaml") 342 | .env() 343 | .sysProps() 344 | .args(args) 345 | .require("/db", HikariConfig.class) 346 | .build(); 347 | ---- 348 | 349 | == jOOQ 350 | 351 | * Type Safe fluent style API for accessing DB 352 | * http://www.jooq.org/doc/3.7/manual/code-generation/codegen-gradle/[Automatic code generation based on your schema] 353 | 354 | .build.gradle 355 | ---- 356 | buildscript { 357 | repositories { 358 | jcenter() 359 | } 360 | dependencies { 361 | classpath 'org.postgresql:postgresql:9.4-1206-jdbc42' 362 | classpath 'org.jooq:jooq-codegen:3.7.1' 363 | classpath 'org.jyaml:jyaml:1.3' 364 | } 365 | } 366 | 367 | dependencies { 368 | runtime 'org.postgresql:postgresql:9.4-1206-jdbc42' 369 | compile 'org.jooq:jooq:3.7.1' 370 | 371 | compile ratpack.dependency('hikari') 372 | } 373 | 374 | import org.jooq.util.jaxb.* 375 | import org.jooq.util.* 376 | import org.ho.yaml.Yaml 377 | 378 | task jooqCodegen { 379 | doLast { 380 | def config = Yaml.load(file('src/ratpack/postgres.yaml')).db 381 | def dsProps = config.dataSourceProperties 382 | 383 | Configuration configuration = new Configuration() 384 | .withJdbc(new Jdbc() 385 | .withDriver("org.postgresql.Driver") 386 | .withUrl("jdbc:postgresql://$dsProps.serverName:$dsProps.portNumber/$dsProps.databaseName") 387 | .withUser(config.username) 388 | .withPassword(config.password)) 389 | .withGenerator(new Generator() 390 | // .withGenerate(new Generate() 391 | // .withImmutablePojos(true) // <1> 392 | // .withDaos(true) // <2> 393 | // .withFluentSetters(true)) // <3> 394 | .withDatabase(new Database() 395 | .withName("org.jooq.util.postgres.PostgresDatabase") 396 | .withIncludes(".*") 397 | .withExcludes("") 398 | .withInputSchema("public")) 399 | .withTarget(new Target() 400 | .withPackageName("jooq") 401 | .withDirectory("src/main/java"))) 402 | 403 | GenerationTool.generate(configuration) 404 | } 405 | } 406 | ---- 407 | <1> Generates immutable POJOs 408 | <2> Generates DAOs 409 | <3> Generates fluent setters for generated Records/POJOs/Interfaces 410 | 411 | === Hikari and jOOQ 412 | 413 | 414 | `DSLContext` provides type-safe fluent API style querying. 415 | jOOQ will responsibly borrow and release connections from the provided `DataSource`. 416 | 417 | [source, java] 418 | .DefaultMeetingRepository.java 419 | ---- 420 | public class DefaultMeetingRepository implements MeetingRepository { 421 | private final DSLContext context; 422 | 423 | @Inject 424 | public DefaultMeetingRepository(DSLContext context) { 425 | this.context = context; 426 | } 427 | 428 | @Override 429 | public Promise> getMeetings() { 430 | return Blocking.get(() -> 431 | context 432 | .select().from(MEETING).fetchInto(Meeting.class) // <1> 433 | ); 434 | } 435 | 436 | @Override 437 | public Operation addMeeting(Meeting meeting) { 438 | return Blocking.op(() -> context.newRecord(MEETING, meeting).store()); 439 | } 440 | } 441 | ---- 442 | <1> `fetchInto(Class)` provides SQL to POJO mapping. POJOs can be generated by jOOQ if desired. 443 | 444 | [source, java] 445 | .JooqModule.java 446 | ---- 447 | public class JooqModule extends AbstractModule { 448 | @Override 449 | protected void configure() { 450 | bind(MeetingRepository.class).to(DefaultMeetingRepository.class).in(Scopes.SINGLETON); 451 | } 452 | 453 | @Provides 454 | @Singleton 455 | public DSLContext dslContext(DataSource dataSource) { 456 | return DSL.using(new DefaultConfiguration().derive(dataSource)); 457 | } 458 | } 459 | ---- 460 | 461 | == Redis 462 | 463 | [source, gradle] 464 | .build.gradle 465 | ---- 466 | dependencies { 467 | compile 'biz.paluch.redis:lettuce:4.0.1.Final' 468 | } 469 | ---- 470 | 471 | [source, yaml] 472 | .redis.yaml 473 | ---- 474 | redis: 475 | host: 192.168.99.100 476 | port: 6379 477 | ---- 478 | 479 | [source, java] 480 | .RedisConfig.java 481 | ---- 482 | public class RedisConfig { 483 | private String url; 484 | 485 | public String getUrl() { 486 | return url; 487 | } 488 | 489 | public void setUrl(String url) { 490 | this.url = url; 491 | } 492 | } 493 | ---- 494 | 495 | [source, java] 496 | .App.java 497 | ---- 498 | RatpackServer.start(ratpackServerSpec -> ratpackServerSpec 499 | .serverConfig(config -> config 500 | .baseDir(BaseDir.find()) 501 | .yaml("postgres.yaml") 502 | .yaml("redis.yaml") 503 | .env() 504 | .sysProps() 505 | .args(args) 506 | .require("/db", HikariConfig.class) 507 | .require("/redis", RedisConfig.class) // <1> 508 | ) 509 | ---- 510 | <1> Add `RedisConfig` to the Registry 511 | 512 | [source, java] 513 | .RedisModule.java 514 | ---- 515 | public class RedisModule extends AbstractModule { 516 | @Override 517 | protected void configure() { } 518 | 519 | @Provides 520 | @Singleton 521 | public RedisClient redisClient(RedisConfig config) { // <1> 522 | return RedisClient.create(config.getUrl()); 523 | } 524 | 525 | @Provides 526 | @Singleton 527 | public StatefulRedisConnection asyncCommands(RedisClient client) { 528 | return client.connect(); 529 | } 530 | 531 | @Provides 532 | @Singleton 533 | public RedisAsyncCommands asyncCommands(StatefulRedisConnection connection) { 534 | return connection.async(); 535 | } 536 | 537 | @Provides 538 | @Singleton 539 | public Service redisCleanup(RedisClient client, StatefulRedisConnection connection) { 540 | return new Service() { // <2> 541 | @Override 542 | public void onStop(StopEvent event) throws Exception { 543 | connection.close(); // <3> 544 | client.shutdown(); // <3> 545 | } 546 | }; 547 | } 548 | } 549 | ---- 550 | <1> Get `RedisConfig` from Registry 551 | <2> `Service` provides an opportunity to hook into Ratpack's start/stop lifecycle events 552 | <3> Cleanup Redis connection and client 553 | 554 | [source, java] 555 | .RatingRepository.java 556 | ---- 557 | public interface RatingRepository { 558 | Promise> getRatings(Long meetingId); 559 | 560 | default Promise getAverageRating(Long meetingId) { 561 | return getRatings(meetingId) 562 | .map(m -> m.entrySet() 563 | .stream() 564 | .map(e -> Pair.of(Integer.valueOf(e.getKey()), Integer.valueOf(e.getValue()))) 565 | .flatMapToInt(pair -> IntStream.range(0, pair.right).map(i -> pair.left)) 566 | .average().orElse(0d) 567 | ); 568 | } 569 | 570 | Operation rateMeeting(String meetingId, String rating); 571 | } 572 | ---- 573 | 574 | [source, java] 575 | .DefaultRatingRepository.java 576 | ---- 577 | public class DefaultRatingRepository implements RatingRepository { 578 | private final RedisAsyncCommands commands; 579 | 580 | @Inject 581 | public DefaultRatingRepository(RedisAsyncCommands commands) { 582 | this.commands = commands; 583 | } 584 | 585 | Function getKeyForMeeting = (id) -> "meeting:" + id + ":rating"; 586 | 587 | @Override 588 | public Promise> getRatings(Long meetingId) { 589 | return Promise.of(downstream -> 590 | commands 591 | .hgetall(getKeyForMeeting.apply(meetingId)) // <1> 592 | .thenAccept(downstream::success) // <2> 593 | ); 594 | } 595 | 596 | @Override 597 | public Operation rateMeeting(String meetingId, String rating) { 598 | return Promise.of(downstream -> 599 | commands.hincrby( 600 | getKeyForMeeting.apply(Long.valueOf(meetingId)), 601 | String.valueOf(rating), 1 602 | ).thenAccept(downstream::success) 603 | ).operation(); 604 | } 605 | } 606 | ---- 607 | <1> Equivalent of `HGETALL meeting:$id:rating` 608 | <2> Signal to downstream consumer that Lettuce is done with async activity 609 | 610 | == Composing data from Postgres and Redis 611 | 612 | [source, java] 613 | .MeetingService.java 614 | ---- 615 | public interface MeetingService { 616 | Promise> getMeetings(); 617 | Operation addMeeting(Meeting meeting); 618 | Operation rateMeeting(String id, String rating); 619 | } 620 | ---- 621 | 622 | [source, java] 623 | .DefaultMeetingService.java 624 | ---- 625 | public class DefaultMeetingService implements MeetingService { 626 | 627 | private final MeetingRepository meetingRepository; 628 | private final RatingRepository ratingRepository; 629 | 630 | public DefaultMeetingService(MeetingRepository meetingRepository, RatingRepository ratingRepository) { 631 | this.meetingRepository = meetingRepository; 632 | this.ratingRepository = ratingRepository; 633 | } 634 | 635 | @Override 636 | public Promise> getMeetings() { 637 | return meetingRepository.getMeetings() 638 | .flatMap(meetings -> 639 | Promise.value( 640 | meetings.stream() 641 | .peek(meeting -> 642 | ratingRepository.getAverageRating(meeting.getId()) 643 | .then(meeting::setRating) // <1> 644 | ) 645 | .collect(Collectors.toList())) 646 | ); 647 | } 648 | 649 | @Override 650 | public Operation addMeeting(Meeting meeting) { 651 | return meetingRepository.addMeeting(meeting); 652 | } 653 | 654 | @Override 655 | public Operation rateMeeting(String id, String rating) { 656 | return ratingRepository.rateMeeting(id, rating); 657 | } 658 | } 659 | ---- 660 | <1> This is naughty, don't perform side effects 661 | 662 | Create a new module to register our `RatingRepository` and `MeetingService` 663 | 664 | [source,java] 665 | .MeetingModule 666 | ---- 667 | public class MeetingModule extends AbstractModule { 668 | @Override 669 | protected void configure() { 670 | } 671 | 672 | @Provides 673 | @Singleton 674 | public RatingRepository ratingRepository(RedisAsyncCommands commands) { 675 | return new DefaultRatingRepository(commands); 676 | } 677 | 678 | @Provides 679 | @Singleton 680 | public MeetingService meetingService(MeetingRepository meetingRepository, RatingRepository ratingRepository) { 681 | return new DefaultMeetingService(meetingRepository, ratingRepository); 682 | } 683 | } 684 | ---- 685 | 686 | [source, java] 687 | .App.java 688 | ---- 689 | public class App { 690 | public static void main(String[] args) throws Exception { 691 | RatpackServer.start(serverSpec -> serverSpec 692 | .serverConfig(/*config*/) 693 | .registry(Guice.registry(bindings -> bindings 694 | .module(HikariModule.class) 695 | .module(JooqModule.class) 696 | .module(RedisModule.class) 697 | .module(MeetingModule.class) // <1> 698 | .bind(MeetingChainAction.class) 699 | )) 700 | .handlers(/*handlers*/) 701 | ); 702 | } 703 | } 704 | ---- 705 | <1> Register our new module with the app 706 | 707 | == Deploying to Heroku 708 | 709 | Main command to execute: 710 | 711 | .Procfile 712 | ---- 713 | web: env DATABASE_URL=$DATABASE_URL build/installShadow/modern-java-web/bin/modern-java-web redis.url=$REDIS_URL 714 | ---- 715 | 716 | Ratpack can pick up config information from just about anywhere. 717 | Here we expose `DATABASE_URL` as an env variable and pass in `REDIS_URL` as `redis.url` as a program arg. 718 | 719 | .Gradle staging task 720 | ---- 721 | task stage(dependsOn: installShadowApp) 722 | ---- 723 | 724 | === Create the Heroku app 725 | 726 | [source, bash] 727 | .bash 728 | ---- 729 | $ heroku create 730 | Creating gentle-beyond-5974... done, stack is cedar-14 // <1> 731 | https://gentle-beyond-5974.herokuapp.com/ | https://git.heroku.com/gentle-beyond-5974 732 | .git // <3> 733 | Git remote heroku added // <2> 734 | heroku-cli: Updating... done. 735 | ---- 736 | <1> `cedar-14` is the Java 8 platform, Heroku's default Java offering 737 | <2> Generated app name `gentle-beyond-5974` 738 | <3> Added git remote named `heroku` 739 | 740 | === Heroku Redis 741 | 742 | https://devcenter.heroku.com/articles/heroku-redis 743 | 744 | 1. Install Plugin ```heroku plugins:install heroku-redis``` 745 | 1. Add to app `heroku addons:create heroku-redis:hobby-dev` 746 | 1. Pass `$REDIS_URL` to your app 747 | 1. Heroku redis-cli `heroku redis:cli` 748 | 749 | === Heroku Postgresql 750 | 751 | https://devcenter.heroku.com/articles/heroku-postgresql 752 | 753 | 1. Add PostgreSQL to app `heroku addons:create heroku-postgresql:hobby-dev` 754 | 1. Wait to come online `heroku pg:wait` 755 | 1. Pass `$DATABASE_URL` to app 756 | 1. Connect to remote `heroku pg:psql` (Requires psql installed locally) 757 | 758 | === Parsing Heroku's Postgres URL 759 | 760 | Heroku exposes the Postgres URL in a format that JDBC cannot parse. 761 | 762 | [source, java] 763 | .HerokuUtils.java 764 | ---- 765 | public interface HerokuUtils { 766 | Function> extractDbProperties = (url) -> { 767 | if (Strings.isNullOrEmpty(url)) return Collections.emptyList(); 768 | 769 | Pattern herokuDbPattern = Pattern 770 | .compile("postgres://(?[^:]+):(?[^:]+)@(?[^:]+):(?[0-9]+)/(?.+)"); // <1> 771 | 772 | Matcher matcher = herokuDbPattern.matcher(url); 773 | if (!matcher.matches()) return Collections.emptyList(); 774 | 775 | return Stream 776 | .of("username", "password", "databaseName", "serverName", "portNumber") 777 | .map(prop -> Pair.of(prop, matcher.group(prop))) // <2> 778 | .map(pair -> pair.left.equals(pair.left.toLowerCase()) ? 779 | pair : Pair.of("dataSourceProperties." + pair.left, pair.right) 780 | ) 781 | .map(pair -> Pair.of("db." + pair.left, pair.right)) 782 | .map(pair -> pair.left + "=" + pair.right) 783 | .collect(Collectors.toList()); 784 | }; 785 | } 786 | ---- 787 | <1> As of Java 7 you can provide group names in regex 788 | <2> We ask for match by group name and construct a `Pair` of property to extracted value 789 | 790 | [source, java] 791 | .App.java 792 | ---- 793 | public class App { 794 | public static void main(String[] args) throws Exception { 795 | List programArgs = Lists.newArrayList(args); 796 | programArgs.addAll( 797 | HerokuUtils.extractDbProperties 798 | .apply(System.getenv("DATABASE_URL")) // <1> 799 | ); 800 | 801 | RatpackServer.start(serverSpec -> serverSpec 802 | .serverConfig(config -> config 803 | .baseDir(BaseDir.find()) 804 | .yaml("postgres.yaml") 805 | .yaml("redis.yaml") 806 | .env() 807 | .sysProps() 808 | .args(programArgs.stream().toArray(String[]::new)) //<2> 809 | .require("/db", HikariConfig.class) 810 | .require("/redis", RedisConfig.class) 811 | ) 812 | .registry(/* registry */) 813 | .handlers(/* handlers */) 814 | ); 815 | } 816 | } 817 | ---- 818 | <1> Extract db properties if present 819 | <2> Pass newly constructed list to Ratpack's server config 820 | 821 | === Push to Heroku 822 | 823 | [source, bash] 824 | ---- 825 | $ git push heroku master 826 | remote: BUILD SUCCESSFUL 827 | remote: 828 | remote: Total time: 40.614 secs 829 | remote: -----> Discovering process types 830 | remote: Procfile declares types -> web 831 | remote: 832 | remote: -----> Compressing... done, 71.3MB 833 | remote: -----> Launching... done, v4 834 | remote: https://gentle-beyond-5974.herokuapp.com/ deployed to Heroku 835 | remote: 836 | remote: Verifying deploy... done. 837 | ---- 838 | 839 | Heroku will see that this is a Gradle project and invoke `./gradlew stage`. 840 | 841 | After the build Heroku will run the command from `Procfile`. 842 | 843 | [source, bash] 844 | ---- 845 | $ heroku open 846 | ---- 847 | 848 | Opens your newly minted webapp in your browser. 849 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'org.postgresql:postgresql:9.4-1206-jdbc42' 7 | classpath 'org.jooq:jooq-codegen:3.7.1' 8 | classpath 'org.jyaml:jyaml:1.3' 9 | } 10 | } 11 | 12 | plugins { 13 | id 'io.ratpack.ratpack-java' version '1.1.1' 14 | id 'com.github.johnrengelman.shadow' version '1.2.2' 15 | id 'idea' 16 | } 17 | 18 | repositories { 19 | jcenter() 20 | } 21 | 22 | mainClassName = 'App' 23 | 24 | task stage(dependsOn: installShadowApp) 25 | 26 | dependencies { 27 | compile 'org.slf4j:slf4j-api:1.7.13' 28 | 29 | runtime 'org.postgresql:postgresql:9.4-1206-jdbc42' 30 | compile 'org.jooq:jooq:3.7.1' 31 | 32 | compile 'biz.paluch.redis:lettuce:4.0.1.Final' 33 | 34 | compile ratpack.dependency('hikari') 35 | 36 | } 37 | 38 | idea { 39 | project { 40 | jdkName = '1.8' 41 | languageLevel = '1.8' 42 | vcs = 'Git' 43 | } 44 | } 45 | 46 | import org.jooq.util.jaxb.* 47 | import org.jooq.util.* 48 | import org.ho.yaml.Yaml 49 | 50 | task jooqCodegen { 51 | doLast { 52 | def config = Yaml.load(file('src/ratpack/postgres.yaml')).db 53 | def dsProps = config.dataSourceProperties 54 | 55 | Configuration configuration = new Configuration() 56 | .withJdbc(new Jdbc() 57 | .withDriver("org.postgresql.Driver") 58 | .withUrl("jdbc:postgresql://$dsProps.serverName:$dsProps.portNumber/$dsProps.databaseName") 59 | .withUser(config.username) 60 | .withPassword(config.password)) 61 | .withGenerator(new Generator() 62 | // .withGenerate(new Generate() 63 | // .withImmutablePojos(true) 64 | // .withDaos(true) 65 | // .withFluentSetters(true)) 66 | .withDatabase(new Database() 67 | .withName("org.jooq.util.postgres.PostgresDatabase") 68 | .withIncludes(".*") 69 | .withExcludes("") 70 | .withInputSchema("public")) 71 | .withTarget(new Target() 72 | .withPackageName("jooq") 73 | .withDirectory("src/main/java"))) 74 | 75 | GenerationTool.generate(configuration) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhyun/modern-java-web/b59635709376d716f275396e25efa4cebbf74968/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 08 00:15:38 EST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This settings file was auto generated by the Gradle buildInit task 3 | * by 'danny' at '12/8/15 12:15 AM' with Gradle 2.9 4 | * 5 | * The settings file is used to specify which projects to include in your build. 6 | * In a single project build this file can be empty or even removed. 7 | * 8 | * Detailed information about configuring a multi-project build in Gradle can be found 9 | * in the user guide at https://docs.gradle.org/2.9/userguide/multi_project_builds.html 10 | */ 11 | 12 | /* 13 | // To declare projects as part of a multi-project build use the 'include' method 14 | include 'shared' 15 | include 'api' 16 | include 'services:webservice' 17 | */ 18 | 19 | rootProject.name = 'modern-java-web' 20 | -------------------------------------------------------------------------------- /src/main/java/App.java: -------------------------------------------------------------------------------- 1 | import com.google.common.collect.Lists; 2 | import com.zaxxer.hikari.HikariConfig; 3 | import jooq.JooqModule; 4 | import meeting.MeetingChainAction; 5 | import meeting.MeetingModule; 6 | import ratpack.guice.Guice; 7 | import ratpack.hikari.HikariModule; 8 | import ratpack.server.BaseDir; 9 | import ratpack.server.RatpackServer; 10 | import redis.RedisConfig; 11 | import redis.RedisModule; 12 | import util.HerokuUtils; 13 | 14 | import java.util.List; 15 | 16 | 17 | public class App { 18 | public static void main(String[] args) throws Exception { 19 | List programArgs = Lists.newArrayList(args); 20 | programArgs.addAll( 21 | HerokuUtils.extractDbProperties 22 | .apply(System.getenv("DATABASE_URL")) 23 | ); 24 | 25 | RatpackServer.start(serverSpec -> serverSpec 26 | .serverConfig(config -> config 27 | .baseDir(BaseDir.find()) 28 | .yaml("postgres.yaml") 29 | .yaml("redis.yaml") 30 | .env() 31 | .sysProps() 32 | .args(programArgs.stream().toArray(String[]::new)) 33 | .require("/db", HikariConfig.class) 34 | .require("/redis", RedisConfig.class) 35 | ) 36 | .registry(Guice.registry(bindings -> bindings 37 | .module(HikariModule.class) 38 | .module(JooqModule.class) 39 | .module(RedisModule.class) 40 | .module(MeetingModule.class) 41 | .bind(MeetingChainAction.class) 42 | )) 43 | .handlers(chain -> chain. 44 | prefix("meeting", MeetingChainAction.class) 45 | ) 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/jooq/JooqModule.java: -------------------------------------------------------------------------------- 1 | package jooq; 2 | 3 | import com.google.inject.AbstractModule; 4 | import com.google.inject.Provides; 5 | import com.google.inject.Scopes; 6 | import meeting.DefaultMeetingRepository; 7 | import meeting.MeetingRepository; 8 | import org.jooq.DSLContext; 9 | import org.jooq.impl.DSL; 10 | import org.jooq.impl.DefaultConfiguration; 11 | 12 | import javax.inject.Singleton; 13 | import javax.sql.DataSource; 14 | 15 | public class JooqModule extends AbstractModule { 16 | @Override 17 | protected void configure() { 18 | bind(MeetingRepository.class).to(DefaultMeetingRepository.class).in(Scopes.SINGLETON); 19 | } 20 | 21 | @Provides 22 | @Singleton 23 | public DSLContext dslContext(DataSource dataSource) { 24 | return DSL.using(new DefaultConfiguration().derive(dataSource)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/jooq/Keys.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This class is generated by jOOQ 3 | */ 4 | package jooq; 5 | 6 | 7 | import javax.annotation.Generated; 8 | 9 | import jooq.tables.Meeting; 10 | import jooq.tables.records.MeetingRecord; 11 | 12 | import org.jooq.Identity; 13 | import org.jooq.UniqueKey; 14 | import org.jooq.impl.AbstractKeys; 15 | 16 | 17 | /** 18 | * A class modelling foreign key relationships between tables of the public 19 | * schema 20 | */ 21 | @Generated( 22 | value = { 23 | "http://www.jooq.org", 24 | "jOOQ version:3.7.1" 25 | }, 26 | comments = "This class is generated by jOOQ" 27 | ) 28 | @SuppressWarnings({ "all", "unchecked", "rawtypes" }) 29 | public class Keys { 30 | 31 | // ------------------------------------------------------------------------- 32 | // IDENTITY definitions 33 | // ------------------------------------------------------------------------- 34 | 35 | public static final Identity IDENTITY_MEETING = Identities0.IDENTITY_MEETING; 36 | 37 | // ------------------------------------------------------------------------- 38 | // UNIQUE and PRIMARY KEY definitions 39 | // ------------------------------------------------------------------------- 40 | 41 | public static final UniqueKey MEETING_PKEY = UniqueKeys0.MEETING_PKEY; 42 | 43 | // ------------------------------------------------------------------------- 44 | // FOREIGN KEY definitions 45 | // ------------------------------------------------------------------------- 46 | 47 | 48 | // ------------------------------------------------------------------------- 49 | // [#1459] distribute members to avoid static initialisers > 64kb 50 | // ------------------------------------------------------------------------- 51 | 52 | private static class Identities0 extends AbstractKeys { 53 | public static Identity IDENTITY_MEETING = createIdentity(Meeting.MEETING, Meeting.MEETING.ID); 54 | } 55 | 56 | private static class UniqueKeys0 extends AbstractKeys { 57 | public static final UniqueKey MEETING_PKEY = createUniqueKey(Meeting.MEETING, Meeting.MEETING.ID); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/jooq/Public.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This class is generated by jOOQ 3 | */ 4 | package jooq; 5 | 6 | 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import javax.annotation.Generated; 12 | 13 | import jooq.tables.Meeting; 14 | 15 | import org.jooq.Sequence; 16 | import org.jooq.Table; 17 | import org.jooq.impl.SchemaImpl; 18 | 19 | 20 | /** 21 | * This class is generated by jOOQ. 22 | */ 23 | @Generated( 24 | value = { 25 | "http://www.jooq.org", 26 | "jOOQ version:3.7.1" 27 | }, 28 | comments = "This class is generated by jOOQ" 29 | ) 30 | @SuppressWarnings({ "all", "unchecked", "rawtypes" }) 31 | public class Public extends SchemaImpl { 32 | 33 | private static final long serialVersionUID = -875771924; 34 | 35 | /** 36 | * The reference instance of public 37 | */ 38 | public static final Public PUBLIC = new Public(); 39 | 40 | /** 41 | * No further instances allowed 42 | */ 43 | private Public() { 44 | super("public"); 45 | } 46 | 47 | @Override 48 | public final List> getSequences() { 49 | List result = new ArrayList(); 50 | result.addAll(getSequences0()); 51 | return result; 52 | } 53 | 54 | private final List> getSequences0() { 55 | return Arrays.>asList( 56 | Sequences.MEETING_ID_SEQ); 57 | } 58 | 59 | @Override 60 | public final List> getTables() { 61 | List result = new ArrayList(); 62 | result.addAll(getTables0()); 63 | return result; 64 | } 65 | 66 | private final List> getTables0() { 67 | return Arrays.>asList( 68 | Meeting.MEETING); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/jooq/Sequences.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This class is generated by jOOQ 3 | */ 4 | package jooq; 5 | 6 | 7 | import javax.annotation.Generated; 8 | 9 | import org.jooq.Sequence; 10 | import org.jooq.impl.SequenceImpl; 11 | 12 | 13 | /** 14 | * Convenience access to all sequences in public 15 | */ 16 | @Generated( 17 | value = { 18 | "http://www.jooq.org", 19 | "jOOQ version:3.7.1" 20 | }, 21 | comments = "This class is generated by jOOQ" 22 | ) 23 | @SuppressWarnings({ "all", "unchecked", "rawtypes" }) 24 | public class Sequences { 25 | 26 | /** 27 | * The sequence public.meeting_id_seq 28 | */ 29 | public static final Sequence MEETING_ID_SEQ = new SequenceImpl("meeting_id_seq", Public.PUBLIC, org.jooq.impl.SQLDataType.BIGINT.nullable(false)); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/jooq/Tables.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This class is generated by jOOQ 3 | */ 4 | package jooq; 5 | 6 | 7 | import javax.annotation.Generated; 8 | 9 | import jooq.tables.Meeting; 10 | 11 | 12 | /** 13 | * Convenience access to all tables in public 14 | */ 15 | @Generated( 16 | value = { 17 | "http://www.jooq.org", 18 | "jOOQ version:3.7.1" 19 | }, 20 | comments = "This class is generated by jOOQ" 21 | ) 22 | @SuppressWarnings({ "all", "unchecked", "rawtypes" }) 23 | public class Tables { 24 | 25 | /** 26 | * The table public.meeting 27 | */ 28 | public static final Meeting MEETING = jooq.tables.Meeting.MEETING; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/jooq/tables/Meeting.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This class is generated by jOOQ 3 | */ 4 | package jooq.tables; 5 | 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import javax.annotation.Generated; 11 | 12 | import jooq.Keys; 13 | import jooq.Public; 14 | import jooq.tables.records.MeetingRecord; 15 | 16 | import org.jooq.Field; 17 | import org.jooq.Identity; 18 | import org.jooq.Table; 19 | import org.jooq.TableField; 20 | import org.jooq.UniqueKey; 21 | import org.jooq.impl.TableImpl; 22 | 23 | 24 | /** 25 | * This class is generated by jOOQ. 26 | */ 27 | @Generated( 28 | value = { 29 | "http://www.jooq.org", 30 | "jOOQ version:3.7.1" 31 | }, 32 | comments = "This class is generated by jOOQ" 33 | ) 34 | @SuppressWarnings({ "all", "unchecked", "rawtypes" }) 35 | public class Meeting extends TableImpl { 36 | 37 | private static final long serialVersionUID = 1370125599; 38 | 39 | /** 40 | * The reference instance of public.meeting 41 | */ 42 | public static final Meeting MEETING = new Meeting(); 43 | 44 | /** 45 | * The class holding records for this type 46 | */ 47 | @Override 48 | public Class getRecordType() { 49 | return MeetingRecord.class; 50 | } 51 | 52 | /** 53 | * The column public.meeting.id. 54 | */ 55 | public final TableField ID = createField("id", org.jooq.impl.SQLDataType.INTEGER.nullable(false).defaulted(true), this, ""); 56 | 57 | /** 58 | * The column public.meeting.organizer. 59 | */ 60 | public final TableField ORGANIZER = createField("organizer", org.jooq.impl.SQLDataType.VARCHAR.length(255), this, ""); 61 | 62 | /** 63 | * The column public.meeting.topic. 64 | */ 65 | public final TableField TOPIC = createField("topic", org.jooq.impl.SQLDataType.VARCHAR.length(255), this, ""); 66 | 67 | /** 68 | * The column public.meeting.description. 69 | */ 70 | public final TableField DESCRIPTION = createField("description", org.jooq.impl.SQLDataType.CLOB, this, ""); 71 | 72 | /** 73 | * Create a public.meeting table reference 74 | */ 75 | public Meeting() { 76 | this("meeting", null); 77 | } 78 | 79 | /** 80 | * Create an aliased public.meeting table reference 81 | */ 82 | public Meeting(String alias) { 83 | this(alias, MEETING); 84 | } 85 | 86 | private Meeting(String alias, Table aliased) { 87 | this(alias, aliased, null); 88 | } 89 | 90 | private Meeting(String alias, Table aliased, Field[] parameters) { 91 | super(alias, Public.PUBLIC, aliased, parameters, ""); 92 | } 93 | 94 | /** 95 | * {@inheritDoc} 96 | */ 97 | @Override 98 | public Identity getIdentity() { 99 | return Keys.IDENTITY_MEETING; 100 | } 101 | 102 | /** 103 | * {@inheritDoc} 104 | */ 105 | @Override 106 | public UniqueKey getPrimaryKey() { 107 | return Keys.MEETING_PKEY; 108 | } 109 | 110 | /** 111 | * {@inheritDoc} 112 | */ 113 | @Override 114 | public List> getKeys() { 115 | return Arrays.>asList(Keys.MEETING_PKEY); 116 | } 117 | 118 | /** 119 | * {@inheritDoc} 120 | */ 121 | @Override 122 | public Meeting as(String alias) { 123 | return new Meeting(alias, this); 124 | } 125 | 126 | /** 127 | * Rename this table 128 | */ 129 | public Meeting rename(String name) { 130 | return new Meeting(name, null); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/jooq/tables/records/MeetingRecord.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This class is generated by jOOQ 3 | */ 4 | package jooq.tables.records; 5 | 6 | 7 | import javax.annotation.Generated; 8 | 9 | import jooq.tables.Meeting; 10 | 11 | import org.jooq.Field; 12 | import org.jooq.Record1; 13 | import org.jooq.Record4; 14 | import org.jooq.Row4; 15 | import org.jooq.impl.UpdatableRecordImpl; 16 | 17 | 18 | /** 19 | * This class is generated by jOOQ. 20 | */ 21 | @Generated( 22 | value = { 23 | "http://www.jooq.org", 24 | "jOOQ version:3.7.1" 25 | }, 26 | comments = "This class is generated by jOOQ" 27 | ) 28 | @SuppressWarnings({ "all", "unchecked", "rawtypes" }) 29 | public class MeetingRecord extends UpdatableRecordImpl implements Record4 { 30 | 31 | private static final long serialVersionUID = 712138885; 32 | 33 | /** 34 | * Setter for public.meeting.id. 35 | */ 36 | public void setId(Integer value) { 37 | setValue(0, value); 38 | } 39 | 40 | /** 41 | * Getter for public.meeting.id. 42 | */ 43 | public Integer getId() { 44 | return (Integer) getValue(0); 45 | } 46 | 47 | /** 48 | * Setter for public.meeting.organizer. 49 | */ 50 | public void setOrganizer(String value) { 51 | setValue(1, value); 52 | } 53 | 54 | /** 55 | * Getter for public.meeting.organizer. 56 | */ 57 | public String getOrganizer() { 58 | return (String) getValue(1); 59 | } 60 | 61 | /** 62 | * Setter for public.meeting.topic. 63 | */ 64 | public void setTopic(String value) { 65 | setValue(2, value); 66 | } 67 | 68 | /** 69 | * Getter for public.meeting.topic. 70 | */ 71 | public String getTopic() { 72 | return (String) getValue(2); 73 | } 74 | 75 | /** 76 | * Setter for public.meeting.description. 77 | */ 78 | public void setDescription(String value) { 79 | setValue(3, value); 80 | } 81 | 82 | /** 83 | * Getter for public.meeting.description. 84 | */ 85 | public String getDescription() { 86 | return (String) getValue(3); 87 | } 88 | 89 | // ------------------------------------------------------------------------- 90 | // Primary key information 91 | // ------------------------------------------------------------------------- 92 | 93 | /** 94 | * {@inheritDoc} 95 | */ 96 | @Override 97 | public Record1 key() { 98 | return (Record1) super.key(); 99 | } 100 | 101 | // ------------------------------------------------------------------------- 102 | // Record4 type implementation 103 | // ------------------------------------------------------------------------- 104 | 105 | /** 106 | * {@inheritDoc} 107 | */ 108 | @Override 109 | public Row4 fieldsRow() { 110 | return (Row4) super.fieldsRow(); 111 | } 112 | 113 | /** 114 | * {@inheritDoc} 115 | */ 116 | @Override 117 | public Row4 valuesRow() { 118 | return (Row4) super.valuesRow(); 119 | } 120 | 121 | /** 122 | * {@inheritDoc} 123 | */ 124 | @Override 125 | public Field field1() { 126 | return Meeting.MEETING.ID; 127 | } 128 | 129 | /** 130 | * {@inheritDoc} 131 | */ 132 | @Override 133 | public Field field2() { 134 | return Meeting.MEETING.ORGANIZER; 135 | } 136 | 137 | /** 138 | * {@inheritDoc} 139 | */ 140 | @Override 141 | public Field field3() { 142 | return Meeting.MEETING.TOPIC; 143 | } 144 | 145 | /** 146 | * {@inheritDoc} 147 | */ 148 | @Override 149 | public Field field4() { 150 | return Meeting.MEETING.DESCRIPTION; 151 | } 152 | 153 | /** 154 | * {@inheritDoc} 155 | */ 156 | @Override 157 | public Integer value1() { 158 | return getId(); 159 | } 160 | 161 | /** 162 | * {@inheritDoc} 163 | */ 164 | @Override 165 | public String value2() { 166 | return getOrganizer(); 167 | } 168 | 169 | /** 170 | * {@inheritDoc} 171 | */ 172 | @Override 173 | public String value3() { 174 | return getTopic(); 175 | } 176 | 177 | /** 178 | * {@inheritDoc} 179 | */ 180 | @Override 181 | public String value4() { 182 | return getDescription(); 183 | } 184 | 185 | /** 186 | * {@inheritDoc} 187 | */ 188 | @Override 189 | public MeetingRecord value1(Integer value) { 190 | setId(value); 191 | return this; 192 | } 193 | 194 | /** 195 | * {@inheritDoc} 196 | */ 197 | @Override 198 | public MeetingRecord value2(String value) { 199 | setOrganizer(value); 200 | return this; 201 | } 202 | 203 | /** 204 | * {@inheritDoc} 205 | */ 206 | @Override 207 | public MeetingRecord value3(String value) { 208 | setTopic(value); 209 | return this; 210 | } 211 | 212 | /** 213 | * {@inheritDoc} 214 | */ 215 | @Override 216 | public MeetingRecord value4(String value) { 217 | setDescription(value); 218 | return this; 219 | } 220 | 221 | /** 222 | * {@inheritDoc} 223 | */ 224 | @Override 225 | public MeetingRecord values(Integer value1, String value2, String value3, String value4) { 226 | value1(value1); 227 | value2(value2); 228 | value3(value3); 229 | value4(value4); 230 | return this; 231 | } 232 | 233 | // ------------------------------------------------------------------------- 234 | // Constructors 235 | // ------------------------------------------------------------------------- 236 | 237 | /** 238 | * Create a detached MeetingRecord 239 | */ 240 | public MeetingRecord() { 241 | super(Meeting.MEETING); 242 | } 243 | 244 | /** 245 | * Create a detached, initialised MeetingRecord 246 | */ 247 | public MeetingRecord(Integer id, String organizer, String topic, String description) { 248 | super(Meeting.MEETING); 249 | 250 | setValue(0, id); 251 | setValue(1, organizer); 252 | setValue(2, topic); 253 | setValue(3, description); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/main/java/meeting/DefaultMeetingRepository.java: -------------------------------------------------------------------------------- 1 | package meeting; 2 | 3 | import org.jooq.DSLContext; 4 | import ratpack.exec.Blocking; 5 | import ratpack.exec.Operation; 6 | import ratpack.exec.Promise; 7 | 8 | import javax.inject.Inject; 9 | import java.util.List; 10 | 11 | import static jooq.tables.Meeting.MEETING; 12 | 13 | public class DefaultMeetingRepository implements MeetingRepository { 14 | private final DSLContext context; 15 | 16 | @Inject 17 | public DefaultMeetingRepository(DSLContext context) { 18 | this.context = context; 19 | } 20 | 21 | @Override 22 | public Promise> getMeetings() { 23 | return Blocking.get(() -> 24 | context 25 | .select() 26 | .from(MEETING) 27 | .fetchInto(Meeting.class) 28 | ); 29 | } 30 | 31 | @Override 32 | public Operation addMeeting(Meeting meeting) { 33 | return Blocking.op(() -> 34 | context.newRecord(MEETING, meeting).store() 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/meeting/DefaultMeetingService.java: -------------------------------------------------------------------------------- 1 | package meeting; 2 | 3 | import ratpack.exec.Operation; 4 | import ratpack.exec.Promise; 5 | import redis.RatingRepository; 6 | 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | 10 | public class DefaultMeetingService implements MeetingService { 11 | 12 | private final MeetingRepository meetingRepository; 13 | private final RatingRepository ratingRepository; 14 | 15 | public DefaultMeetingService(MeetingRepository meetingRepository, RatingRepository ratingRepository) { 16 | this.meetingRepository = meetingRepository; 17 | this.ratingRepository = ratingRepository; 18 | } 19 | 20 | @Override 21 | public Promise> getMeetings() { 22 | return meetingRepository.getMeetings() 23 | .flatMap(meetings -> 24 | Promise.value( 25 | meetings.stream() 26 | .peek( meeting -> 27 | ratingRepository.getAverageRating(meeting.getId()) 28 | .then(meeting::setRating) 29 | ) 30 | .collect(Collectors.toList())) 31 | ); 32 | } 33 | 34 | @Override 35 | public Operation addMeeting(Meeting meeting) { 36 | return meetingRepository.addMeeting(meeting); 37 | } 38 | 39 | @Override 40 | public Operation rateMeeting(String id, String rating) { 41 | return ratingRepository.rateMeeting(id, rating); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/meeting/Meeting.java: -------------------------------------------------------------------------------- 1 | package meeting; 2 | 3 | public class Meeting { 4 | private Long id; 5 | private String organizer; 6 | private String topic; 7 | private String description; 8 | 9 | private double rating; 10 | 11 | public Long getId() { 12 | return id; 13 | } 14 | 15 | public void setId(Long id) { 16 | this.id = id; 17 | } 18 | 19 | public String getOrganizer() { 20 | return organizer; 21 | } 22 | 23 | public void setOrganizer(String organizer) { 24 | this.organizer = organizer; 25 | } 26 | 27 | public String getTopic() { 28 | return topic; 29 | } 30 | 31 | public void setTopic(String topic) { 32 | this.topic = topic; 33 | } 34 | 35 | public String getDescription() { 36 | return description; 37 | } 38 | 39 | public void setDescription(String description) { 40 | this.description = description; 41 | } 42 | 43 | public double getRating() { 44 | return rating; 45 | } 46 | 47 | public void setRating(double rating) { 48 | this.rating = rating; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/meeting/MeetingChainAction.java: -------------------------------------------------------------------------------- 1 | package meeting; 2 | 3 | import ratpack.func.Action; 4 | import ratpack.handling.Chain; 5 | import ratpack.jackson.Jackson; 6 | import ratpack.path.PathTokens; 7 | 8 | 9 | public class MeetingChainAction implements Action { 10 | @Override 11 | public void execute(Chain chain) throws Exception { 12 | chain 13 | .path(ctx -> { 14 | MeetingService service = ctx.get(MeetingService.class); 15 | ctx 16 | .byMethod(method -> method 17 | .get(() -> service 18 | .getMeetings() 19 | .map(Jackson::json) 20 | .then(ctx::render) 21 | ) 22 | .post(() -> ctx 23 | .parse(Jackson.fromJson(Meeting.class)) 24 | .nextOp(service::addMeeting) 25 | .map(m -> "Added meeting for " + m.getOrganizer()) 26 | .then(ctx::render) 27 | ) 28 | ); 29 | }) 30 | .get(":id:\\d+/rate/:rating:[1-5]", ctx -> { 31 | MeetingService service = ctx.get(MeetingService.class); 32 | PathTokens pathTokens = ctx.getPathTokens(); 33 | service 34 | .rateMeeting(pathTokens.get("id"), pathTokens.get("rating")) 35 | .then(() -> ctx.redirect("/meeting")); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/meeting/MeetingModule.java: -------------------------------------------------------------------------------- 1 | package meeting; 2 | 3 | import com.google.inject.AbstractModule; 4 | import com.google.inject.Provides; 5 | import com.lambdaworks.redis.api.async.RedisAsyncCommands; 6 | import redis.DefaultRatingRepository; 7 | import redis.RatingRepository; 8 | 9 | import javax.inject.Singleton; 10 | 11 | public class MeetingModule extends AbstractModule { 12 | @Override 13 | protected void configure() { 14 | } 15 | 16 | @Provides 17 | @Singleton 18 | public RatingRepository ratingRepository(RedisAsyncCommands commands) { 19 | return new DefaultRatingRepository(commands); 20 | } 21 | 22 | @Provides 23 | @Singleton 24 | public MeetingService meetingService(MeetingRepository meetingRepository, RatingRepository ratingRepository) { 25 | return new DefaultMeetingService(meetingRepository, ratingRepository); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/meeting/MeetingRepository.java: -------------------------------------------------------------------------------- 1 | package meeting; 2 | 3 | import ratpack.exec.Operation; 4 | import ratpack.exec.Promise; 5 | 6 | import java.util.List; 7 | 8 | public interface MeetingRepository { 9 | Promise> getMeetings(); 10 | Operation addMeeting(Meeting meeting); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/meeting/MeetingService.java: -------------------------------------------------------------------------------- 1 | package meeting; 2 | 3 | import ratpack.exec.Operation; 4 | import ratpack.exec.Promise; 5 | 6 | import java.util.List; 7 | 8 | public interface MeetingService { 9 | Promise> getMeetings(); 10 | Operation addMeeting(Meeting meeting); 11 | Operation rateMeeting(String id, String rating); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/redis/DefaultRatingRepository.java: -------------------------------------------------------------------------------- 1 | package redis; 2 | 3 | import com.lambdaworks.redis.api.async.RedisAsyncCommands; 4 | import ratpack.exec.Operation; 5 | import ratpack.exec.Promise; 6 | 7 | import javax.inject.Inject; 8 | import java.util.Map; 9 | import java.util.function.Function; 10 | 11 | public class DefaultRatingRepository implements RatingRepository { 12 | private final RedisAsyncCommands commands; 13 | 14 | @Inject 15 | public DefaultRatingRepository(RedisAsyncCommands commands) { 16 | this.commands = commands; 17 | } 18 | 19 | Function getKeyForMeeting = (id) -> "meeting:" + id + ":rating"; 20 | 21 | @Override 22 | public Promise> getRatings(Long meetingId) { 23 | return Promise.of(downstream -> 24 | commands 25 | .hgetall(getKeyForMeeting.apply(meetingId)) 26 | .thenAccept(downstream::success) 27 | ); 28 | } 29 | 30 | @Override 31 | public Operation rateMeeting(String meetingId, String rating) { 32 | return Promise.of(downstream -> 33 | commands.hincrby( 34 | getKeyForMeeting.apply(Long.valueOf(meetingId)), 35 | String.valueOf(rating), 1 36 | ).thenAccept(downstream::success) 37 | ).operation(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/redis/RatingRepository.java: -------------------------------------------------------------------------------- 1 | package redis; 2 | 3 | import ratpack.exec.Operation; 4 | import ratpack.exec.Promise; 5 | import ratpack.func.Pair; 6 | 7 | import java.util.Map; 8 | import java.util.stream.IntStream; 9 | 10 | public interface RatingRepository { 11 | Promise> getRatings(Long meetingId); 12 | 13 | default Promise getAverageRating(Long meetingId) { 14 | return getRatings(meetingId) 15 | .map(m -> m.entrySet() 16 | .stream() 17 | .map(e -> Pair.of(Integer.valueOf(e.getKey()), Integer.valueOf(e.getValue()))) 18 | .flatMapToInt(pair -> IntStream.range(0, pair.right).map(i -> pair.left)) 19 | .average().orElse(0d) 20 | ); 21 | } 22 | 23 | Operation rateMeeting(String meetingId, String rating); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/redis/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package redis; 2 | 3 | public class RedisConfig { 4 | private String url; 5 | 6 | public String getUrl() { 7 | return url; 8 | } 9 | 10 | public void setUrl(String url) { 11 | this.url = url; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/redis/RedisModule.java: -------------------------------------------------------------------------------- 1 | package redis; 2 | 3 | import com.google.inject.AbstractModule; 4 | import com.google.inject.Provides; 5 | import com.lambdaworks.redis.RedisClient; 6 | import com.lambdaworks.redis.api.StatefulRedisConnection; 7 | import com.lambdaworks.redis.api.async.RedisAsyncCommands; 8 | import ratpack.server.Service; 9 | import ratpack.server.StopEvent; 10 | 11 | import javax.inject.Singleton; 12 | 13 | public class RedisModule extends AbstractModule { 14 | @Override 15 | protected void configure() { } 16 | 17 | @Provides 18 | @Singleton 19 | public RedisClient redisClient(RedisConfig config) { 20 | return RedisClient.create(config.getUrl()); 21 | } 22 | 23 | @Provides 24 | @Singleton 25 | public StatefulRedisConnection asyncCommands(RedisClient client) { 26 | return client.connect(); 27 | } 28 | 29 | @Provides 30 | @Singleton 31 | public RedisAsyncCommands asyncCommands(StatefulRedisConnection connection) { 32 | return connection.async(); 33 | } 34 | 35 | @Provides 36 | @Singleton 37 | public Service redisCleanup(RedisClient client, StatefulRedisConnection connection) { 38 | return new Service() { 39 | @Override 40 | public void onStop(StopEvent event) throws Exception { 41 | connection.close(); 42 | client.shutdown(); 43 | } 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/util/HerokuUtils.java: -------------------------------------------------------------------------------- 1 | package util; 2 | 3 | import com.google.common.base.Strings; 4 | import ratpack.func.Pair; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.function.Function; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | 15 | public interface HerokuUtils { 16 | Function> extractDbProperties = (url) -> { 17 | if (Strings.isNullOrEmpty(url)) return Collections.emptyList(); 18 | 19 | Pattern herokuDbPattern = Pattern 20 | .compile("postgres://(?[^:]+):(?[^:]+)@(?[^:]+):(?[0-9]+)/(?.+)"); 21 | 22 | Matcher matcher = herokuDbPattern.matcher(url); 23 | if (!matcher.matches()) return Collections.emptyList(); 24 | 25 | return Stream 26 | .of("username", "password", "databaseName", "serverName", "portNumber") 27 | .map(prop -> Pair.of(prop, matcher.group(prop))) 28 | .map(pair -> pair.left.equals(pair.left.toLowerCase()) ? 29 | pair : Pair.of("dataSourceProperties." + pair.left, pair.right) 30 | ) 31 | .map(pair -> Pair.of("db." + pair.left, pair.right)) 32 | .map(pair -> pair.left + "=" + pair.right) 33 | .collect(Collectors.toList()); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/ratpack/.ratpack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhyun/modern-java-web/b59635709376d716f275396e25efa4cebbf74968/src/ratpack/.ratpack -------------------------------------------------------------------------------- /src/ratpack/postgres.yaml: -------------------------------------------------------------------------------- 1 | db: 2 | dataSourceClassName: org.postgresql.ds.PGSimpleDataSource 3 | username: postgres 4 | password: password 5 | dataSourceProperties: 6 | databaseName: modern 7 | serverName: 192.168.99.100 8 | portNumber: 5432 9 | -------------------------------------------------------------------------------- /src/ratpack/redis.yaml: -------------------------------------------------------------------------------- 1 | redis: 2 | url: redis://192.168.99.100:6379 3 | --------------------------------------------------------------------------------