Serving jokes locally with Ratpack and MongoDB

In two previous posts, I discussed applications I created that were simple client-side front ends for the Internet Chuck Norris Database (ICNDB), located at http://icndb.com. This post gives the details of the local server I created, using Groovy, MongoDB, and the cool Ratpack project (note new URL). The earlier posts contained parts of that app, but since then I’ve updated it to the latest version of Ratpack, revised the gradle build file accordingly, added a couple of integration tests, and checked the whole thing into GitHub.

I often use ICNDB in my Groovy presentations, because it’s easy to access, returns a simple JSON object, and is rather amusing.

(This, by the way, is in direct contrast to Mr. Norris himself, whose politics are somewhat to the right of Attila the Hun. Still, I only care about the jokes themselves, which are pretty funny.)

Accessing the site is trivial:
[sourcecode language=”groovy”]
import groovy.json.JsonSlurper

def url = ‘http://api.icndb.com/jokes/random’
def json = new JsonSlurper().parseText(url.toURL().text)
def joke = json?.value?.joke
println joke
[/sourcecode]
The Groovy JDK adds a toURL method to java.lang.String, which returns an instance of java.net.URL. It also adds a getText method to java.net.URL that returns the entire response as a string. I use the parseText method in JsonSlurper to parse it and just dig through the properties to get the contained joke.

Of course, my demo relies on that site being available, and that’s far from a sure thing, especially if Carlos Ray’s lawyers ever get to it. It also has a tendency to go down at inconvenient intervals, like when I’m trying to present the above script.

(Quick aside: this blog is hosted on WordPress, which has a much better uptime record than my actual home page. My site is hosted on a very old Windows laptop in my office. You’ve heard of “five nines” uptime, where a site guarantees it’ll be available 99.999% of the time? My site only promises “nine fives” uptime. It’s online a bit more than half the time.)

It behooves me, therefore, to keep a local copy if possible. That turns out to be pretty easy. First, there aren’t a whole lot of jokes. The ICNDB API offers the link http://api.icndb.com/jokes/count to return the total number of jokes. That request returns a JSON object:
[sourcecode language=”javascript”]
{
"type": "success",
"value": 546
}
[/sourcecode]
I can certainly handle a database of that size locally. In fact, in my app I load the whole thing into memory because why not?

Since the format of the jokes is JSON, I decided to use a MongoDB database to store them. The native format for MongoDB is BSON, or binary JSON, so all I have to do is grab the jokes and I can just append them to a local database. To make the code easier, I use the GMongo project, which is just a Groovy wrapper around Mongo’s Java client library.

The other useful method in the ICNDB API is http://api.icndb.com/jokes/random/num, where num represents the number of jokes you want returned. I want all of them, so I replace num with the total.

For example, if I access http://api.icndb.com/jokes/random/5, I get something like:
[sourcecode language=”javascript”]
{
"type": "success",
"value": [
{
"id": 204,
"joke": "Nagasaki never had a bomb dropped on it. Chuck Norris jumped out of a plane and punched the ground",
"categories": []
},
{
"id": 329,
"joke": "There are only two things that can cut diamonds: other diamonds, and Chuck Norris.",
"categories": []
},
{
"id": 348,
"joke": "There?s an order to the universe: space, time, Chuck Norris…. Just kidding, Chuck Norris is first.",
"categories": []
},
{
"id": 360,
"joke": "Two wrongs don’t make a right. Unless you’re Chuck Norris. Then two wrongs make a roundhouse kick to the face.",
"categories": []
},
{
"id": 406,
"joke": "Chuck Norris doesn’t say "who’s your daddy", because he knows the answer.",
"categories": []
}
]
}
[/sourcecode]
The only difference from the original URL is that now the value property returns all of the contained jokes, but that’s not a problem. The overall script is therefore:
[sourcecode language=”groovy”]
import groovy.json.JsonSlurper
import com.gmongo.GMongo

// Drop the current icndb database, if it exists
GMongo mongo = new GMongo()
def db = mongo.getDB(‘icndb’)
db.cnjokes.drop()

// Get the total number of available jokes
JsonSlurper slurper = new JsonSlurper()
String jsonTxt = ‘http://api.icndb.com/jokes/count’.toURL().text
def json = slurper.parseText(jsonTxt)
int total = json.value.toInteger()

// Grab all of them at once
jsonTxt = "http://api.icndb.com/jokes/random/${total}".toURL().text
json = slurper.parseText(jsonTxt)

// Save them all locally
def jokes = json.value
jokes.each {
db.cnjokes << it
}
assert total == jokes*.id.size()
assert total == db.cnjokes.find().count()
[/sourcecode]
How cool is it that all I have to do is grab the jokes and append them to the collection to save them in the database? Truly, we live in magical times. 🙂

I can browse the database in Eclipse if I use the MonjaDB plugin. Here’s a screenshot showing it:
MonjaDB

Now that the database is populated, I can build the Ratpack app. I started off using the lazybones builder inside of gvm, the Groovy enVironment Manager, which I discussed in the earlier post. Ratpack keeps evolving, though, and lazybones hasn’t kept up, so the changes I’ve made to the resulting app are a bit more substantial than I originally intended.

Here’s the JokeServer class. In the constructor, I load all the jokes into a Groovy map, where the keys are the id’s and the values are the joke strings themselves.
[sourcecode language=”groovy”]
@Singleton
class JokeServer {
GMongo mongo = new GMongo()
Map jokes = [:]
List ids = []

JokeServer() {
DB db = mongo.getDB(‘icndb’)
def jokesInDB = db.cnjokes.find([categories: ‘nerdy’])
jokesInDB.each { j ->
jokes[j.id] = j.joke
}
ids = jokes.keySet() as List
}

[/sourcecode]
The other method in the class returns a random joke by shuffling the id’s and returning the associated joke. (The id’s themselves aren’t consecutive, partly because I’m only using the “nerdy” ones and partly because the original site skipped ID values.)
[sourcecode language=”groovy”]

String getJoke(String firstName = ‘Chuck’,
String lastName = ‘Norris’) {
Collections.shuffle(ids)
String joke = jokes[ids[0]]
if (!joke) println "Null joke at id=$id"
if (firstName != ‘Chuck’)
joke = joke.replaceAll(/Chuck/, firstName)
if (lastName != ‘Norris’)
joke = joke.replaceAll(/Norris/, lastName)
return joke
}
}
[/sourcecode]
I’m using Groovy’s nice optional arguments mechanism here, so if I invoke getJoke without arguments I get the original joke, but if I supply a first name or a last name they’re used in the joke itself.

Here’s my test case to make sure at least this much is working. It’s a regular JUnit test, implemented in Groovy.
[sourcecode language=”groovy”]
import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;

class JokeServerTest {
JokeServer server = JokeServer.instance // @Singleton on server

@Test
public void testGetJokeFirstNameLastName() {
String joke = server.getJoke(‘Patton’, ‘Boggs’)
assert !joke.contains(‘Chuck’)
assert !joke.contains(‘Norris’)
assert joke.contains(‘Patton’)
assert joke.contains(‘Boggs’)
}

@Test
public void testGetJoke() {
String joke = server.joke
assert joke
}

}
[/sourcecode]
Did you notice that I added @Singleton on the JokeServer, just for fun? That’s why the test grabs the server using the instance property. My first test then uses the strings “Patton” and “Boggs”, the name of the law firm that sent me the takedown notice. The second test accesses the joke property, which calls the getJoke method by the normal Groovy idiom.

Ratpack applications use a script called ratpack.groovy to set the various handlers. Since my script is so simple, I just added everything to that handler:
[sourcecode language=”groovy”]
import static ratpack.groovy.Groovy.*

import com.kousenit.JokeServer

JokeServer server = JokeServer.instance

ratpack {
handlers {
get {
String message
if(request.queryParams.firstName ||
request.queryParams.lastName) {
message = server.getJoke(
request.queryParams.firstName,
request.queryParams.lastName)
} else {
message = server.joke
}
response.headers.set ‘Content-Type’, ‘application/json’
response.send message
}

assets "public"
}
}
[/sourcecode]
The ratpack method takes a closure containing the various handlers. I only have a single handler, which is accessed using a get request. Then I get the relevant joke, set the Content-Type header to the MIME type for JSON, and return it. Actually, since my server returns the actual string, I probably shouldn’t set the header at all, but it hasn’t hurt anything so far.

The only difference between this script and the one I showed previously is the import statement at the top. Now the ratpack method is a static method in the ratpack.groovy.Groovy class, which Eclipse (actually, Groovy / Grails Tool Suite) can’t find even though it’s in the dependencies.

The next piece of the puzzle is the Gradle build script itself. Ratpack recently changed its package structure and moved its deployed versions to JFrog. Here’s the updated build script:
[sourcecode language=”groovy”]
apply plugin: "ratpack-groovy"

buildscript {
repositories {
maven { url "http://oss.jfrog.org/repo&quot; }
mavenCentral()
}
dependencies {
classpath ‘io.ratpack:ratpack-gradle:0.9.0-SNAPSHOT’
}
}

repositories {
maven { url "http://oss.jfrog.org/repo&quot; }
mavenCentral()
maven { url "http://repo.springsource.org/repo&quot; } // for springloaded
}

dependencies {
compile ‘com.gmongo:gmongo:1.0’
testCompile "org.spockframework:spock-core:0.7-groovy-2.0", {
exclude module: "groovy-all"
}

// SpringLoaded enables runtime hot reloading.
springloaded "org.springsource.springloaded:springloaded-core:1.1.4"
}

task wrapper(type: Wrapper) {
gradleVersion = "1.8"
}
[/sourcecode]
The buildscript block includes the information for downloading the ratpack-groovy plugin, which has changed the group id of the ratpack dependency to io.ratpack. I excluded the groovy-all module from the Spock dependency because it’s already part of the ratpack-groovy plugin, and I updated the Gradle version property in the wrapper to 1.8, but otherwise this is the same as the one generated by lazybones.

Before I run, though, I still want some sort of integration test. Most of the available examples online don’t have any tests (sigh), and I spent far too many hours figuring out how to get one to work, so I’m including it here.
[sourcecode language=”groovy”]
package com.kousenit

import ratpack.groovy.test.LocalScriptApplicationUnderTest
import ratpack.groovy.test.TestHttpClient
import spock.lang.Specification

class ServerIntegrationSpec extends Specification {

def aut = new LocalScriptApplicationUnderTest()
@Delegate TestHttpClient client = aut.httpClient()

def setup() {
resetRequest()
}

def "regular get request returns Chuck Norris string"() {
when:
String result = get(‘/’).asString()

then:
println result
result.contains(‘Chuck Norris’)
}

def "firstName and lastName parameters work"() {
when:
def response = get(‘?firstName=Carlos&lastName=Ray’)?.asString()

then:
println response
response.contains(‘Carlos Ray’)
}
}
[/sourcecode]
As with most Spock tests, the tests themselves are pretty self-explanatory. The hard part was figuring out the proper URLs to invoke and knowing to use the asString method, which wasn’t obvious at all. I’m also not clear on the mechanism used to get the TestHttpClient instance, but I’m sure Luke Daley will explain it to me when I see him next. 🙂

One last quirk should be noted for anyone trying to duplicate this. To run the server, I type:

> gradle run

which starts up the server on port 5050. Access the server using http://localhost:5050 or http://localhost:5050/firstName=Carlos&lastName=Ray and you’re all set.

For some reason, the script that runs the app requires the ratpack.groovy script to be located in the src/ratpack folder. If I want to run the tests, however,

> gradle test

then I have to have the ratpack.groovy file in the root of the project. I have no idea why, nor do I know how to configure them so I only need one and not the other, so I just copied the file into both locations.

(I know — that’s ugly and awkward. I promise to fix it when find out why it’s necessary in the first place.)

So, at long last, if you want the code, it’s all stored in my GitHub repository at https://github.com/kousen/cnjokeserver. Feel free to clone it and wreak whatever havoc upon it you desire.

——-

For those who might be interested, a few notes about my book, Making Java Groovy:

  • The book page at Amazon now has 24 reviews. All are four or five stars but one, which is extremely gratifying. More importantly, the text of each makes it clear that the book is finding its intended audience: Java developers who want to make their jobs easier by adding Groovy.
  • The lone three-star review doesn’t say anything bad about the book anyway, so at least I’ve got that going for me, which is nice.
  • If you get the book at Amazon rather than at Manning, you can still get the ebook versions (pdf, mobi, and epub). The mechanism to do so is included in an insert in the book.
  • The response to my silly marketing ideas in my last couple of posts has been silence punctuated by the occasional cricket, so I’m going to stop doing that unless something really good occurs to me. Oh well. They made me laugh during the writing process, and anything that keeps you writing is a Good Thing(TM).

5 responses to “Serving jokes locally with Ratpack and MongoDB”

  1. Hi there,

    Thanks for the nice article!

    About the problem with the duplicate ratpack.groovy file, you can try adding an empty ratpack.properties file under /src/ratpack and delete ratpack.groovy from the root of the project.

    This works for me.

    Regards,
    Dimitris

  2. Nice, though I have to say I have no idea why that works or what made you think to try it. I have noticed an occasional empty ratpack.properties file in other projects and wondered what it was for.

  3. I also had noticed ratpack.properties in other projects on GitHub with a somewhat related comment embedded in them (e.g. [0]), as well a relevant post in the Ratpack forum [1].

    As for why it works, I think one of the Ratpack GitHub Issues provides a brief explanation about this [2].

    [0] https://github.com/ratpack/example-ratpack-gradle-groovy-app/blob/master/src/ratpack/ratpack.properties
    [1] http://forum.ratpack.io/Functional-testing-using-Geb-td176.html
    [2] https://github.com/ratpack/ratpack/issues/96

  4. Reblogged this on Andrey Hihlovskiy and commented:
    Very entertaining way to learn Ratpack and MongoDB!

  5. The problem with Eclipse is that it’s compiling ratpack.groovy into the bin folder (or whatever output folder Eclpse is using). If you look, you should see a ratpack.class file therein.

    I addressed this (I’m using the Groovy-Eclipse plugin) by enabling script folder support under Groovy->Compiler preferences. By default (once enabled), it will simply copy groovy files under the resources folder, which fixes the problem. If this doesn’t work for you, you could explicitly specify src/ratpack, or src/main/resources/ratpack.groovy, or whatever.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from Stuff I've learned recently...

Subscribe now to keep reading and get access to the full archive.

Continue reading