Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 4.0

So, for the 2nd, (actually third installment, but the JIRB one doesn't really count), I was thinking that we should take a look at a few different subjects. The red thread will be a simple web based Blog application built with Camping. Camping is a microframework for web development, by whytheluckystuff, is insanely small and incredibly powerful. It uses another library called Markaby, which generates markup based on pure Ruby code. I will show the application in smaller parts, with explanations, and at the end include a link to the complete source file.

First of all we have to have a working JRuby built from the latest version of trunk. After JRuby is working we need to install a few gems. Follow these commands and you'll be good:

No Format
jruby %JRUBY_HOME%\bin\gem install camping --no-ri --no-rdoc --include-dependencies
jruby %JRUBY_HOME%\bin\gem install ActiveRecord-JDBC --no-ri --no-rdoc

This installs Camping, ActiveRecord, ActiveSupport, Markaby, Builder, Metaid and ActiveRecord-JDBC. We don't generate RDoc or RI for these gems, since that's one part of JRuby that still is pretty slow.

The Blog application

The first thing the blog application needs is a database. I will use MySQL, but other databases may work through the JDBC-adapter, but there is still some work to be done in this area. I will have my MySQL server on localhost, so change the configuration if you do not have this setup. You'll need a database for the blog application. I've been conservative and named the database "blog" and the user "blog" with the password "blog". Easy to remember, but not so good for production.

Update: As azzoti mentioned, you have to set your classpath to include the MySQL JDBC-driver, which can be downloaded from http://www.mysql.org/.

Now, open up a new Ruby file and call it blog.rb. The name of the file is important; it has to have the same name as the Camping application. Now, first of all we include the dependencies:

No Format
require 'rubygems'
require 'camping'
require 'camping/session'
require_gem 'ActiveRecord-JDBC'
require 'active_record/connection_adapters/jdbc_adapter'
require 'java'

include_class 'java.lang.System'

These statements include Rubygems, Camping, Camping session support, ActiveRecord-JDBC and Java support. It also includes the Java class named java.lang.System for later use. The next step is to include some configuration for our application.

No Format
Camping.goes :Blog
Blog::Models::Base.establish_connection :adapter => 'jdbc', :driver => 'com.mysql.jdbc.Driver', :url => 'jdbc:mysql://localhost:3306/blog', :username => 'blog', :password => 'blog'

module Blog
  include Camping::Session
end

These statements first names our application with the Camping.goes-statement. This includes some fairly heavy magic, including reopening the file and rewriting the source to include more references to the Camping framework. But this line is all that is needed. The next line establishes our connection to the database, and it follows standard JDBC naming of the parameters. Of course, these should be in a YAML file somewhere, but for now we make it easy. The last part makes sure we have Session support in our Blog application.

Now we need to define our model, and this is easily done since Camping uses ActiveRecord:

No Format
module Blog::Models
  def self.schema(&block)
    @@schema = block if block_given?
    @@schema
  end

  class Post < Base; belongs_to :user; end
  class Comment < Base; belongs_to :user; end
  class User < Base; end
end

The first part of this code defines a helper method that either sets the schema to the block given, or returns an earlier defined schema. The second part defines our model, which includes Post, Comment and User, and their relationships.

The schema is also part of the application, and we'll later see that Camping can automatically create it if it doesn't exist (that's why we didn't need to create any tables ourselves, just the database).

No Format
Blog::Models.schema do
  create_table :blog_posts, :force => true do |t|
    t.column :id,       :integer, :null => false
    t.column :user_id,  :integer, :null => false
    t.column :title,    :string,  :limit => 255
    t.column :body,     :text
  end
  create_table :blog_users, :force => true do |t|
    t.column :id,       :integer, :null => false
    t.column :username, :string
    t.column :password, :string
  end
  create_table :blog_comments, :force => true do |t|
    t.column :id,       :integer, :null => false
    t.column :post_id,  :integer, :null => false
    t.column :username, :string
    t.column :body,     :text
  end
end

This defines the three tables needed by our blog system. Note that the names of the tables include the name of the application as a prefix. This is because Camping expects more than one application to be deployed in the same container, using the same database.

When we have defined the schema, it's time to define our controller actions. In Camping, each action is a class, and each action class define a method for get, one for post, etc. These classes will be defined inside the module Blog::Controllers. The first action we create will be the Index action. It looks like this:

No Format
class Index < R '/'
  def get
    @posts = Post.find :all
    render :index
  end
end

This defines a class that inherits from an anonymous class defined by the R method. What it really does, is bind the Index action to the /-path. It uses ActiveRecord to get all posts and then renders the view with the name index.

The Add-action adds a new Post, but only if there is a user in the @state-variable, which acts as a session. If something is posted to it, it creates a new Post from the information and redirects to the View-action:

No Format
class Add
  def get
    unless @state.user_id.blank?
      @user = User.find @state.user_id
      @post = Post.new
    end
    render :add
  end

  def post
    post = Post.create :title => input.post_title, :body => input.post_body, :user_id => @state.user_id
    redirect View, post
  end
end

As you can see, there's not much to it. Instance variables in the controller will be available to the view later on. Note that this action doesn't inherit from any class at all. This means it will only be available by an URL with it's name in it.

We need a few more controllers. View and Edit are for handling Posts. Comment adds new comments to an existing Post. Login and Logout are pretty self explanatory. And Style returns a stylesheet for all pages. Note that Style doesn't render anything, it just sets @body to a string with the contents to return.

No Format
class View < R '/view/(\d+)'
  def get post_id
    @post = Post.find post_id
    @comments = Models::Comment.find :all, :conditions => ['post_id = ?', post_id]
    render :view
  end
end

class Edit < R '/edit/(\d+)', '/edit'
  def get post_id
    unless @state.user_id.blank?
      @user = User.find @state.user_id
    end
    @post = Post.find post_id
    render :edit
  end

  def post
    @post = Post.find input.post_id
    @post.update_attributes :title => input.post_title, :body => input.post_body
    redirect View, @post
  end
end

class Comment
  def post
    Models::Comment.create(:username => input.post_username, :body => input.post_body, :post_id => input.post_id)
        redirect View, input.post_id
  end
end

class Login
  def post
    @user = User.find :first, :conditions => ['username = ? AND password = ?', input.username, input.password]
    if @user
      @login = 'login success !'
      @state.user_id = @user.id
    else
      @login = 'wrong user name or password'
    end
    render :login
  end
end

class Logout
  def get
    @state.user_id = nil
    render :logout
  end
end

class Style < R '/styles.css'
  def get
    @headers["Content-Type"] = "text/css; charset=utf-8"
    @body = %{
      a {
        text-decoration: none;
      }
      a:hover {
        text-decoration: underline;
      }
      body {
        font-family: Utopia, Georga, serif;
      }
      h1.header {
        background-color: #fef;
        margin: 0; padding: 10px;
      }
      div.content {
        padding: 10px;
      }
      div.post {
        background-color: #ffa;
        border: 1px solid black;
        padding: 20px;
        margin: 10px;
      }
    }
  end
end

Also note how easy it is to define routing rules with the help of regular expressions to the R method.

Next up, we have to create our views. Since Camping uses Markaby, we do it in Ruby, in the same file. Views are methods in the module Blog::Views with the same name as referenced inside the controllers call to render. There is a special view called layout which get called for all views, if you don't specify otherwise in the call to render. It looks like this:

No Format
def layout
  html do
    head do
      title 'Blog'
      link :rel => 'stylesheet', :type => 'text/css', :href => '/styles.css', :media => 'screen'
    end
    body do
      h1.header { a 'Blog', :href => R(Index) }
      div.content do
        self << yield
      end
    end
  end
end

As you can see, standard HTML tags are defined by calling a method by that name. The contents of the tag is created inside the block sent to that method, and if it makes sense to give it content as an argument, this works too. Title, for example. If you give a block to it, it will evaluate this and add the result as the title, but in this case it's easier to just provide a string. Note how a link is created, by the method R (another method R this time, since this is in the Views module). Finally, the contents of the layout gets added by appending the result of a yield to self.

The index view is the first we'll see when visiting the application, and it looks like this:

No Format
def index
  if @posts.empty?
    p 'No posts found.'
  else
    for post in @posts
      _post(post)
    end
  end
  p { a 'Add', :href => R(Add) }
  p "Current time in millis is #{System.currentTimeMillis}."
end

I've also added a call that writes out the current time in milliseconds, from Java's System class, to show that we're actually in Java land now, and potentially could base much of our application on data from Java. We check if there are any posts, and if so iterate over them and write them out with a partial called _post. We also create a link to add more posts. The rest of the views look like this:

No Format
def login
  p { b @login }
  p { a 'Continue', :href => R(Add) }
end

def logout
  p "You have been logged out."
  p { a 'Continue', :href => R(Index) }
end

def add
  if @user
    _form(post, :action => R(Add))
  else
    _login
  end
end

def edit
  if @user
    _form(post, :action => R(Edit))
  else
    _login
  end
end

def view
  _post(post)
  p "Comment for this post:"
  for c in @comments
    h1 c.username
    p c.body
  end

  form :action => R(Comment), :method => 'post' do
    label 'Name', :for => 'post_username'; br
    input :name => 'post_username', :type => 'text'; br
    label 'Comment', :for => 'post_body'; br
    textarea :name => 'post_body' do; end; br
    input :type => 'hidden', :name => 'post_id', :value => post.id
    input :type => 'submit'
  end
end

And the three partials:

No Format
def _login
  form :action => R(Login), :method => 'post' do
    label 'Username', :for => 'username'; br
    input :name => 'username', :type => 'text'; br
    label 'Password', :for => 'password'; br
    input :name => 'password', :type => 'text'; br

    input :type => 'submit', :name => 'login', :value => 'Login'
  end
end

def _post(post)
  div.post do
    h1 post.title
    p post.body
    p do
      a "Edit", :href => R(Edit, post)
      a "View", :href => R(View, post)
    end
  end
end

def _form(post, opts)
  p do
    text "You are logged in as #{@user.username} | "
    a 'Logout', :href => R(Logout)
  end
  form({:method => 'post'}.merge(opts)) do
    label 'Title', :for => 'post_title'; br
    input :name => 'post_title', :type => 'text', :value => post.title; br
    label 'Body', :for => 'post_body'; br
    textarea post.body, :name => 'post_body'; br

    input :type => 'hidden', :name => 'post_id', :value => post.id
    input :type => 'submit'
  end
end

In my opinion, this code is actually much easier to read than HTML and most of it is fairly straight forward. One interesting part is the add and edit methods, which checks if a user is logged in, otherwise uses the _login-partial instead of showing the real content.

Finally, we will define a create-method for Camping, which is responsible for creating the tables for our model:

No Format
def Blog.create
  Camping::Models::Session.create_schema
  unless Blog::Models::Post.table_exists?
    ActiveRecord::Schema.define(&Blog::Models.schema)
    Blog::Models::User.create(:username => 'admin', :password => 'camping')
  end
end

This first creates a table for the session information, and then checks if the Post-table exists; if not all the tables in the schema defined before is created.

Now you have seen the complete application. If you have no interest in writing this by hand, the complete code can be found here.

Running the application

To run a Camping application, you need to run the camping executable that has been installed into your %JRUBY_HOME%\bin on your application file. In my case I run it like this:

No Format
jruby %JRUBY_HOME%\bin\camping blog.rb

in the directory where my blog.rb exists and I very soon have a nice application at http://localhost:3301/blog which works wonderfully. Startup is a little bit slow, but as soon as WEBrick has started listening the application is very snappy. You can try changing your blog.rb-file too; Camping will automatically update your application without having to restart the server. As I said above, I included a call to System.currentTimeMillis, to show that we are actually using Java in this blog-application. If that isn't apparent from the call to System, remember that we are actually using JDBC to talk to our database, and very soon you will be able to use the ActiveRecord-JDBC adapter to connect to any databases Java can talk too. That's a bright future.