Before I get to the good parts of this post (the technical content), let me take care of a few marketing issues.
First, as I mentioned in my last post, I gave my “Making Java Groovy” presentation at JavaOne last week. If you were there and you haven’t completed your Session Surveys, please do so. I really want to Party Like a (JavaOne) Rock Star, Party Like a (JavaOne) Rock Star, etc., though I expect in reality that would involve sharing a quiet dinner at home with my wife. She probably wouldn’t appreciate it if I trashed the room, too, at least not more than it’s already trashed*.
*Yeah, my 21 year old son still does live at home, why do you ask?
That did give me the opportunity to see my book on the shelf of a book store for the first time:
I should also mention that as of 9/30, if you buy the book at Manning, you can now get all the electronic formats (pdf, mobi, and epub).
Finally, I got my first Amazon review today, and was so good I walked around with a smile all day.
Now on to the real content. In my Groovy presentations, I often like to access a RESTful web service and show (1) how easy it is to make a GET request using Groovy, and (2) how to use a JsonSlurper
to parse a JSON response to get the information inside it. For this purpose my default site is ICNDB, the Internet Chuck Norris Database.
That site has caused me problems, though. First, it resulted in my first ever take down notice from a lawyer, which I’ll show in a moment. Seriously. As part of my Android presentations at No Fluff, Just Stuff conferences I build a simple app to access that site, parse the results (using a GSON parser, since I don’t have Groovy available) and update the display. When running, the app looks like:
To make it easy for the attendees of my Android talks to play with, I uploaded the app to the Google Play store. That was fun until I received the following email, from an address I’ll leave out:
Dear Sir/Madam:
Patton Boggs LLP represents Carlos Ray Norris, aka Chuck Norris, the famous actor and celebrity.
We are contacting you because we recently learned that you have developed and are distributing a software application that uses Mr. Norris’s name and/or image without authorization on Google Play.
Mr. Norris appreciates all of his fans. However, the unauthorized use of his name and/or image severely harms my client and jeopardizes his existing business relationships.
Mr. Norris owns legal rights in his name and image which includes copyright, trademark, and publicity rights (the “Norris Properties”). He uses the Norris Properties regularly in his own business endeavors. Therefore we have asked Google to remove your application from Google Play because it violates Mr. Norris’s intellectual property rights.
We request that you (1) immediately stop developing and distributing “Chuck Norris” applications; (2) remove all “Chuck Norris” applications that you have developed or control from all websites under your control; and (3) do not use Mr. Norris’s name or image, or any cartoon or caricature version of Mr. Norris’s name or image for any endeavor, including in connection with software applications, without Mr. Norris’s permission.
Thank you for honoring Mr. Norris’s legal rights. Please contact me if you have questions.
Sincerely, …
A few points immediately come to mind:
- Carlos Ray Norris? Seriously? I had no idea.
- Does it matter that my app is free and only consumes data from a publicly available web site? Apparently not.
- If I changed the icon and replaced the words “Chuck” and “Norris” everywhere with the words “Patton” and “Boggs” (the name of the law firm :)), could I upload it again?
I’m still not exactly sure what I was doing wrong, but of course I did not want to cross Carlos Ray “Chuck” Norris, to say nothing of his lawyers. But after talking to a few people, I decided to ignore the letter, at least until any money was involved.
I haven’t heard anything since, but about a week later Google Play took down my app.
So be it. If you want the source code, though, check out the ICNDB project in my GitHub repository. It’s mostly just a demonstration of how to Spring’s RestTemplate
, Google’s GSON parser, and an Android AsyncTask
together. In the repo is another branch that uses a Java Timer
to refresh the joke every 10 seconds.
The real question is, why haven’t the barracudas lawyers gone after the original ICNDB site? Worse, what happens to my poor app (and, more importantly, my presentation) when they do?
I decided my best course of action was to download as many of the jokes as possible and be ready to serve them up locally in case I need them. That, at long last, brings me to the technical part of this blog post.
The original web site returns jokes in JSON format. That means storing them in a MongoDB database is trivial, because Mongo’s native format is BSON (binary JSON) and I can even query on the joke properties later.
How do I grab all the jokes? There’s no obvious query in the API for that, but there is a work around. If I first access http://api.icndb.com/jokes/count, I can get the total number of jokes. Then there is a URL called http://api.icndb.com/jokes/random/:num, which fetches num
random jokes. According to the web site, it returns them in the form:
{ "type": "success", "value": [ { "id": 1, "joke": "Joke 1" }, { "id": 5, "joke": "Joke 5" }, { "id": 9, "joke": "Joke 9" } ] }
The value
property is a list containing all the individual jokes.
To work with MongoDB, I’ll use the driver from the GMongo project. It follows the typical Groovy idiom, in that it takes an existing Java API (in this case, the ugly and awkward Java driver for Mongo) and wraps it in a much simpler Groovy API. As a beautiful illustration of the process, here’s an excerpt from the com.gmongo.GMongo
class:
[sourcecode language=”groovy”]
class GMongo {
@Delegate
Mongo mongo
// … lots of overloaded constructors …
DB getDB(String name) {
patchAndReturn mongo.getDB(name)
}
static private patchAndReturn(db) {
DBPatcher.patch(db); return db
}
}
[/sourcecode]
Note the use of the @Delegate
annotation, which exposes all the methods on the existing Java-based Mongo
class through the GMongo
wrapper.
Based on that, here’s my script to download all the jokes and store them in a local MongoDB database:
[sourcecode language=”groovy”]
import groovy.json.*
import com.gmongo.GMongo
GMongo mongo = new GMongo()
def db = mongo.getDB(‘icndb’)
db.cnjokes.drop()
String jsonTxt = ‘http://api.icndb.com/jokes/count’.toURL().text
def json = new JsonSlurper().parseText(jsonTxt)
int total = json.value.toInteger()
jsonTxt = "http://api.icndb.com/jokes/random/${total}".toURL().text
json = new JsonSlurper().parseText(jsonTxt)
def jokes = json.value
jokes.each {
db.cnjokes << it
}
println db.cnjokes.find().count()
[/sourcecode]
Note the nice overloaded left-shift operator to add each joke to the collection.
I’ve run this script several times and I consistently get 546 total jokes. That means the entire collection easily fits in memory, a fact I take advantage of when serving them up myself.
My client needs to request a random joke from the server. I want to do this in a public forum, so I’m not interested in any off-color jokes. Also, the ICNDB server itself offers a nice option that I want to duplicate. If you specify a firstName
or lastName
property on the URL, the joke will replace the words “Chuck” and “Norris” with what you specify. I like this because, well, the more I find out about Carlos Ray the more I wish I didn’t know.
Here’s my resulting JokeServer
class:
[sourcecode language=”groovy”]
package com.kousenit
import java.security.SecureRandom
import com.gmongo.GMongo
import com.mongodb.DB
@Singleton
class JokeServer {
GMongo mongo = new GMongo()
Map jokes = [:]
List ids = []
JokeServer() {
DB db = mongo.getDB(‘icndb’)
def jokesInDB = db.cnjokes.find([categories: [$ne : ‘explicit’]])
jokesInDB.each { j ->
jokes[j.id] = j.joke
}
ids = jokes.keySet() as List
}
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]
MongoDB has a JavaScript API which uses qualifiers like $ne
for “not equals”. The Groovy API wrapper lets me add those as keys in a map supplied to the find
method.
I added the @Singleton
annotation on the class, though that may be neither necessary or appropriate. I may want multiple instances of this class for scaling purposes. I’ll have to think about that. Let me know if you have an opinion.
I’m sure there’s an easier way to get a random joke out of the collection, but I kept running into issues when I tried using random integers. This way works, though it’s kind of ugly.
The getJoke
method uses Groovy’s cool optional arguments capability. If getJoke
is invoked with no arguments, I’ll use the original version. If firstName
and/or lastName
are specified, I use the replaceAll
method from the Groovy JDK to change the value in the joke. Strings are still immutable, though, so the replaceAll
method returns a new object and I have to reuse my joke
reference to point to it. I actually missed that the first time (sigh), but that’s what test cases are for.
Speaking of which, here’s the JUnit test (written in Groovy) to verify the JokeServer
is working properly:
[sourcecode language=”groovy”]
package com.kousenit
import static org.junit.Assert.*
import org.junit.Before
import org.junit.Test
class JokeServerTest {
JokeServer server = JokeServer.instance
@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() {
assert server.joke
}
}
[/sourcecode]
I can invoke the getJoke
method with zero, one, or two arguments. The basic testGetJoke
method uses zero arguments. Or, rather, it accesses the joke
property, which then calls getJoke()
using the usual Groovy idiom.
Now I need to use this class inside a web server, and that gave me a chance to dig into the ratpack project. Ratpack is a Groovy project and I’ve checked out the source code from GitHub, but there’s a simpler way to create a new ratpack application based on the Groovy enVironment Manager (gvm) tool.
First I used gvm
to install lazybones
:
> gvm install lazybones
Then I created my ratpack project:
> lazybones create ratpack icndb
That creates a simple structure (shown in the ratpack manual) with a Gradle build file. The only modification I made to build.gradle
was to add “compile 'com.gmongo:gmongo:1.0'
” to the dependencies block.
Here’s the file ratpack.groovy
, the script that configures the server:
[sourcecode language=”groovy”]
import static org.ratpackframework.groovy.RatpackScript.ratpack
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
}
}
}
[/sourcecode]
I configured only a single “get
” handler, which is invoked on an HTTP GET request. I check to see if either a firstName
or lastName
query parameter is supplied, and, if so, I invoke the full getJoke
method. Otherwise I just access the joke
property. I made sure to set the Content-Type
header in the response to indicate I was returning JSON data, and sent the message.
If I type ./gradlew run
from the command line, ratpack starts up its embedded Netty server on port 5050 and serves jokes on demand. Here’s a picture of the default request and response (using the Postman plugin in Chrome):
Here is the response if I specify a first and last name:
There you have it. I wanted to write a test inside ratpack to check my hander, but the infrastructure for that appears to still be under development. I tried to imitate one of the samples and extend the ScriptAppSpec
class, but that class isn’t in my dependent jars. I also tried to follow the Rob Fletcher’s mid-century ipsum example, but while I was able to create the test, it failed when running because it couldn’t find the ratpack.groovy
file inside the ratpack folder, since it expected it to be in the root. The right approach would probably be to turn the contents of my get
block into a separate Handler
, because the manual talks about how to test them, but I haven’t done that yet.
As they say, if you live on the cutting edge, sometimes you get cut. I expect that will all settle down by 1.0.
For my presentations, I now need to update my Android client so that it accesses http://localhost:5050 and understands that the JSON coming back is a bit simpler than that served up by the original server. That’s beyond the scope of this (way too long) blog post, however.
I haven’t yet committed the application to GitHub, but I will eventually. In the meantime I just have to hope that my new app also doesn’t run afoul of Carlos, Ray, Patton, and Boggs, Esq.
Leave a Reply