Okay, I’m lying about the Cockney Rhyming slang – I just needed a third thing that started with ‘C’. But I do have good things regarding the HTML5 Canvas, which lets us draw in a browser from JavaScript:
- First, a cheatsheet of Canvas functions and members, for those of us with short memories.
- Next, a fairly simple example of drawing a curvy shape in a Canvas.
- And lastly, the Wikipedia entry on Bézier curves. Scroll down past the first batch of math to the “Constructing Bézier curves” section – the animations there really helped me visualize what’s going on here.
Ruby, and more specifically Ruby on Rails, clearly provides lot of power without writing much code. But there’s a price – you have to know what code to write. I’ve found that one disadvantage to “Convention Over Configuration” is that if you don’t know the convention, the system is generally not going to help you out.
I’ve recently gone through the effort of working out, step by step, how to create a gem that contains Ruby code, adapt that code to be served out using Rails, and then move the entire web service into a gem. Once we’ve done this, we can add all the capability (and REST access points) provided by the gem with a single line in our rails application’s Gemfile.
Step 1: An “hola” gem that outputs to the console
This is really just a modified version of the “hola” gem put forth in the RubyGuides tutorial.
# File: hola_curtis.gemspec
Gem::Specification.new do |s|
s.name = 'hola_curtis'
s.version = '0.0.0'
s.summary = "Hola!"
s.description = "A simple hello world gem"
s.authors = ["Nick Quaranto","Curtis Lacy"]
s.email = 'curtis.lacy@grgcomponents.com'
s.files = ["lib/hola_curtis.rb"]
s.homepage =
'http://rubygems.org/gems/hola'
end
I’ve removed the “Date” line from the example at the advice of the Ruby 1.9.3 Documentation.
# File: lib/hola_curtis.rb
class HolaCurtis
def self.hi
puts "Hello World!"
end
end
Once you have both these files, you can build the gem and test it in the Interactive Ruby Shell:
toko:testgem cmlacy$ rvm use 1.9.2 toko:testgem cmlacy$ cd hola_curtis toko:hola_curtis cmlacy$ gem build hola_curtis.gemspec toko:hola_curtis cmlacy$ gem install ./hola_curtis-0.0.0.gem toko:hola_curtis cmlacy$ irb toko:hola_curtis cmlacy$ irb 1.9.2-p318 :001 > require 'hola_curtis' => true 1.9.2-p318 :002 > HolaCurtis.hi Hello World! => nil 1.9.2-p318 :003 > quit toko:hola_curtis cmlacy$
Step 2: Adjust the hola gem so that it can output to a rails app.
First, make a rails app to serve out the gem output:
toko:testgem cmlacy$ rvm use 1.9.2 toko:testgem cmlacy$ rails new serve-hola
Now we have a basic server.
Add this line to the gemfile:
gem 'hola_curtis'
Then run bundle install
remove public/index.html
enable the default route in config/routes.rb by uncommenting this line:
root :to => 'welcome#index'
Running rails server and going to http://localhost:3000 in a browser will present an error page: “uninitialized constant WelcomeController”
Running rake routes/code> gives us a list of the routes:
root / welcome#index
So generate a controller:
toko:serve-hola cmlacy$ rails generate controller Welcome index
Now we can see a page when we run the server. Edit app/views/welcome/index.html.erb:
<h1>Welcome#index! Hola output is: <%= HolaCurtis.hi %></h1> <p>Find me in app/views/welcome/index.html.erb</p>
Now when we hit the page from the server, the gem output goes to the console! Edit the gem and reinstall it.
# File: lib/hola_curtis.rb
class HolaCurtis
def self.hi
"Hello World!" # Removed the 'puts', so this string is just returned.
end
end
Running the interactive shell again, we can verify that the string is being returned rather than printed:
toko:hola_curtis cmlacy$ gem build hola_curtis.gemspec Successfully built RubyGem Name: hola_curtis Version: 0.0.0 File: hola_curtis-0.0.0.gem toko:hola_curtis cmlacy$ gem install ./hola_curtis-0.0.0.gem Successfully installed hola_curtis-0.0.0 1 gem installed Installing ri documentation for hola_curtis-0.0.0... Installing RDoc documentation for hola_curtis-0.0.0... toko:hola_curtis cmlacy$ irb 1.9.2-p318 :001 > require 'hola_curtis' => true 1.9.2-p318 :002 > HolaCurtis.hi => "Hello World!" 1.9.2-p318 :003 > quit
Go back to serve-hola and update the gem:
toko:serve-hola cmlacy$ bundle install toko:serve-hola cmlacy$ rails server
Done! Now when you connect with a browser, you'll see that the gem output is in the index file.
Step 3: Create a new Hola gem contains its own .erb template and route
So now we have a gem which outputs its content to a rails app, but we haven't encapsulated the templates themselves in the gem yet.
First, Make a new enginex gem scaffold. (From Smashing Magazine)
toko:testgem cmlacy$ gem install enginex toko:testgem cmlacy$ enginex hola_plugin
This generates a new scaffold.
Modify the files to add our capability, then build and install the gem.
First, add this to the end of hola_plugin/Gemfile
gemspec
And change these two lines in hola_plugin/hola_plugin.gemspec:
s.summary = "hola_plugin-#{s.version}"
s.description = "Adds the \"Hello World!\" page to any Rails 3 application."
Now we can move some code from our rails application into the gem:
serve-hola/app/controllers/welcome_controller.rb -> hola_plugin/app/controllers/hola_plugin/welcome_controller.rb serve-hola/app/views/welcome/index.html.erb -> hola_plugin/app/views/hola_plugin/welcome/index.html.erb
index.html.erb is going to need a little change:
<h1>Welcome#index! Hola output is: <%= HolaPlugin::WelcomeController.hi %></h1> <p>Find me in app/views/welcome/index.html.erb</p>
Make a route file that defines where our gem's .erb template will be invoked:
# File: hola_plugin/config/routes.rb Rails.application.routes.draw do get "hola" => "hola_plugin/welcome#index" , :as => :hola_page end
And create an engine that ties the gem in:
# File: hola_plugin/lib/hola_plugin/engine.rb
module HolaPlugin
class Engine < Rails::Engine
initializer "team_page.load_app_instance_data" do |app|
HolaPlugin.setup do |config|
config.app_root = app.root
end
end
initializer "team_page.load_static_assets" do |app|
app.middleware.use ::ActionDispatch::Static, "#{root}/public"
end
end
end
Change our rails app's Gemfile to include 'hola_plugin' rather than 'hola_curtis':
gem 'hola_plugin'
Remove the default route from our rails app (in serve-hola/config/routes.rb), to make sure we don't stimulate any old code we have lying around:
# You can have the root of your site routed with "root" # just remember to delete public/index.html. # root :to => 'welcome#index'
Rebuild and install the gem, and run rails server in server_hola. Now, when you point your browser at http://localhost:3000/hola, you'll see the content served directly from your gem! Just including the gem will set up the routes, load the templates, everything!
Step 4: What if I don't like the .erb template included with the gem?
If you don't like the default template included in the gem, you can override it in your rails app. This is seamless. Just create the overriding file, and it works fine.
Put the following in serve-hola/app/views/hola_plugin/welcome/index.html.erb:
<h1>CHANGED! Hola output is: <%= HolaPlugin::WelcomeController.hi %></h1> <p>Find me in app/views/hola_plugin/welcome/index.html.erb</p>
Re-run rails server and refresh your browser. Now you should see the overridden content, without having to change anything else about the gem.
When Twitter decided to shut down their basic authentication service a few weeks ago, it provided the impetus I needed to start figuring out how to use OAuth. Dogan Berktas’s article and source code were a real godsend on this, but something bothered me about it:
(In LoginServlet.java:)
HttpSession session = request.getSession();
session.setAttribute("token", token);
session.setAttribute("tokenSecret", tokenSecret);
While it works fine, putting the secret tokens in the session makes me uneasy. I’ve reworked the code a bit to store the secret keys in GAE’s persistent datastore, instead. Here’s the result:
1. You’ll need to follow the setup instructions that Mr. Berktas lays out here: http://doganberktas.com/2010/01/26/using-twitter-oauth-on-gaej/
2. Add persistence support to your application (src/META-INF/persistence.xml):
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">
<persistence-unit name="mydata">
<provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
<properties>
<property name="datanucleus.NontransactionalRead" value="true" />
<property name="datanucleus.NontransactionalWrite" value="true" />
<property name="datanucleus.appengine.autoCreateDatastoreTxns" value="false"/>
<property name="datanucleus.ConnectionURL" value="appengine" />
<property name="datanucleus.jpa.addClassTransformer" value="false" />
</properties>
</persistence-unit>
</persistence>
3. Create an object which can store the request keys:
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class TwitterRequestToken {
@Id
private String requestTokenKey;
private String requestTokenSecret;
private long lastUsed;
public String getRequestTokenKey() {
return requestTokenKey;
}
public void setRequestTokenKey(String requestTokenKey) {
this.requestTokenKey = requestTokenKey;
}
public String getRequestTokenSecret() {
return requestTokenSecret;
}
public void setRequestTokenSecret(String requestTokenSecret) {
this.requestTokenSecret = requestTokenSecret;
}
public long getLastUsed() {
return lastUsed;
}
public void setLastUsed(long lastUsed) {
this.lastUsed = lastUsed;
}
}
4. Since the EnityManagerFactory is slow to create, and reusable in both servlets, you’ll need a singleton to create and manage it:
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public class SingletonEntityManagerFactory
{
private static final EntityManagerFactory def = Persistence.createEntityManagerFactory( "mydata" );
public static EntityManagerFactory getDefault()
{
return def;
}
}
5. For convenience’s sake, I moved the Consumer Key and Consumer Secret (the ones Twitter gives you when you register your application) into static constants:
public class TwitterSettings {
/**
* Twitter OAuth Consumer Key.
*/
public static final String CONSUMER_KEY = "<your consumer key here>";
/**
* Twitter OAuth Consumer Secret.
*/
public static final String CONSUMER_SECRET = "<your consumer secret here>";
}
6. And at last, the updated servlets themselves:
LoginServlet.java:
import java.io.IOException;
import java.io.PrintWriter;
import javax.persistence.EntityManager;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.http.RequestToken;
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
try {
Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(TwitterSettings.CONSUMER_KEY,
TwitterSettings.CONSUMER_SECRET);
RequestToken requestToken = twitter.getOAuthRequestToken();
storeToken( requestToken );
PrintWriter out = response.getWriter();
try {
response.setContentType("text/html;charset=UTF-8");
out.print("<a href='" + requestToken.getAuthorizationURL()
+ "'/>Sign in with Twitter</a>");
} finally {
out.close();
}
} catch (TwitterException te) {
if (401 == te.getStatusCode()) {
System.out.println("Unable to get the access token.");
} else {
te.printStackTrace();
}
}
}
protected synchronized void storeToken(RequestToken requestToken)
{
EntityManager em = SingletonEntityManagerFactory.getDefault().createEntityManager();
try {
em.getTransaction().begin();
TwitterRequestToken token = new TwitterRequestToken();
token.setLastUsed( System.currentTimeMillis() );
token.setRequestTokenKey( requestToken.getToken() );
token.setRequestTokenSecret( requestToken.getTokenSecret() );
em.persist( token );
}
catch( Exception e )
{
em.getTransaction().rollback();
}
finally
{
if( em.getTransaction().isActive() )
{
em.getTransaction().commit();
}
}
em.close();
}
}
HomeServlet.java:
import java.io.IOException;
import java.io.PrintWriter;
import javax.persistence.EntityManager;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.User;
import twitter4j.http.AccessToken;
public class HomeServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
Twitter twitter = new TwitterFactory().getInstance();
PrintWriter out = resp.getWriter();
try {
twitter.setOAuthConsumer( TwitterSettings.CONSUMER_KEY, TwitterSettings.CONSUMER_SECRET );
EntityManager em = SingletonEntityManagerFactory.getDefault().createEntityManager();
TwitterRequestToken requestToken = em.find( TwitterRequestToken.class, req.getParameter( "oauth_token" ));
AccessToken accessToken = twitter.getOAuthAccessToken(
requestToken.getRequestTokenKey(), requestToken.getRequestTokenSecret() );
twitter.setOAuthAccessToken(accessToken);
User user = twitter.verifyCredentials();
HttpSession session = req.getSession();
session.setAttribute("twitter-user", user);
//
try {
resp.setContentType("text/html;charset=UTF-8");
//
out.println("hello " + user.getName() + " from "
+ user.getLocation());
out.println("Followers : " + user.getFollowersCount());
out.println("Following : " + user.getFriendsCount());
out.println("Status count : " + user.getStatusesCount());
} finally {
out.close();
}
} catch (TwitterException e) {
e.printStackTrace();
}
}
}
7. I am, regrettably, leaving cleaning up the datastore as an exercise for the reader. Each request token in the datastore has a timestamp – you’ll need to check them over periodically and clear out the ones that haven’t been used in a while.

