text: Serving jokes locally with Ratpack and MongoDB
  November 15, 2013 5 Comments, 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:
  import groovy.json.JsonSlurper def url = '' def json = new JsonSlurper().parseText(url.toURL().text) def joke = json?.value?.joke println joke
  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 to return the total number of jokes. That request returns a JSON object:
  { "type": "success", "value": 546 }
  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, 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, I get something like:
  { "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": [] } ] }
  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:
  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 = ''.toURL().text def json = slurper.parseText(jsonTxt) int total = json.value.toInteger() // Grab all of them at once jsonTxt = "{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()
  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:
  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.
  @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 } ...
  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.)
  ... 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 } }
  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.
  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 } }
  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:
  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" } }
  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:
  apply plugin: "ratpack-groovy" buildscript { repositories { maven { url "" } mavenCentral() } dependencies { classpath 'io.ratpack:ratpack-gradle:0.9.0-SNAPSHOT' } } repositories { maven { url "" } mavenCentral() maven { url "" } // 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" }
  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.
  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') } }
  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 or 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. 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).
  Recent Comments