Brian's Waste of Time

Tue, 15 Feb 2005

Re: Rails for Struts-ters

Got some feedback on my previous post which is probably best reprinted (now that I've gotten permission) verbatim:

Great article on the comparison of Struts and Rails. 
I have a few pointers on how you can make it even better.

1. Rename app/views/layout/scaffold.rb to app/views/layout/application.rb -- 
then its automatically picked up by all controllers.

2. Using model :user is not necessary unless you don't follow the 
User => user.rb model. When you do (and the generators ensure that you do 
pretty much all the time), the User model is automatically included when the 
class is referenced.

3. These are not necessary: helper_attr :return_to, :user_login. All instance 
variables assigned in a controller action is automatically made available to the view.

I'm not sure what the pass_thru is about, but in any case, here's a more idiomatic 
Rails controller for your example:

class LoginController < ApplicationController
  def index
    if @session['user'] = User.authenticate(@params['user_login'])
      logger.info "User logged in: #{@session['user']}"
      redirect_to_path(@params['return_to'])
    else
      @session['return_to'] = @params['return_to']
      redirect_to :controller => 'rsvp',
                  :action => 'select_invitation',
                  :params => { 'name' => @params['user_login'] }
    end
  end

  def pass_thru
    @return_to = @params['return_to']
    render_action 'login'
  end

  def logout
    reset_session
    redirect_to :controller => 'information', :action => 'index'
  end
end
--
David Heinemeier Hansson

Nice when the guy who wwrote the framework is poking at your code ;-)

0 writebacks [/src/java] permanent link

Brian For Sale!

This is wrong in so many ways...

Brian Mcallister on Ebay

I am tempted to bid myself up...

2 writebacks [/stuff] permanent link

Rails for Struts-ters (Part 1)

After looking at the various introductory tutorials for RubyOnRails, a lot of people seem to come away thinking it is a simple CRUD framework -- mostly because of Scaffolding. A common theme seems to be, "well, it looks fast for slapping together a prototype, but when you need control, you it ain't there." I can see why people think that way, after watching the 10-minute video, and the reading the ONLamp tutorial. So here is a stab at a guided tour, using Struts as a point of reference. The 10,000m view of the two is very similar, so this should play out nicely. It is important to note that this is an imperfect comparison in one big regard -- it only really looks at one part of Rails, ActionPack. ActiveRecord, ActionMailer, and the upcoming SOAP/XML-RPC stuff map more along the lines of OJB (or Hibernate, or Cayenne, or JDO, etc), Spring's JavaMail hooks, and Axis respectively.

On to the good stuff.

In both cases the core design is a Request -> Filters -> Action -> Filters -> View pipeline. In Struts you get something that looks like:

/*
 * Copyright 2000-2004 Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.struts.webapp.example;

/* imports omitted */

/**
 * Validate a user logon.
 */
public final class LogonAction extends BaseAction {

    static String USERNAME = "username";
    static String PASSWORD = "password";

    User getUser(UserDatabase database, String username,
                           String password, ActionMessages errors) 
            throws ExpiredPasswordException {
        User user = null;
        if (database == null){
            errors.add(
                ActionMessages.GLOBAL_MESSAGE,
                new ActionMessage("error.database.missing"));
        }
        else {
            user = database.findUser(username);
            if ((user != null) && !user.getPassword().equals(password)) {
                user = null;
            }
            if (user == null) {
                errors.add(
                    ActionMessages.GLOBAL_MESSAGE,
                    new ActionMessage("error.password.mismatch"));
            }
        }
        return user;
    }

    void SaveUser(HttpServletRequest request, User user) {

        HttpSession session = request.getSession();
        session.setAttribute(Constants.USER_KEY, user);
        if (log.isDebugEnabled()) {
            log.debug(
                "LogonAction: User '"
                    + user.getUsername()
                    + "' logged on in session "
                    + session.getId());
        }
    }

    public ActionForward execute(
        ActionMapping mapping,
        ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response)
        throws Exception {

        // Local variables
        UserDatabase database = getUserDatabase(request);
        String username = (String) PropertyUtils.getSimpleProperty(form,
                USERNAME);
        String password = (String) PropertyUtils.getSimpleProperty(form,
                PASSWORD);
        ActionMessages errors = new ActionMessages();

        // Retrieve user
        User user = getUser(database,username,password,errors);

        // Save (or clear) user object
        SaveUser(request,user);

        // Report back any errors, and exit if any
        if (!errors.isEmpty()) {
            this.saveErrors(request, errors);
            return (mapping.getInputForward());
        }

        // Otherwise, return "success"
        return (findSuccess(mapping));
    }
}

(note that I am taking all Struts sample code from the struts-mailreader.war example app bundled with Struts).

With rails you get something that looks like:

class LoginController < ApplicationController
  layout 'scaffold'
  model :user
  helper_attr :return_to, :user_login

  def index
    redirect_to :action => 'login'
  end

  def pass_thru
    @return_to = @params['return_to']
    render_action 'login'
  end

  def login
    @user_login = @params['user_login']
    @return_to = @params['return_to']
    
    valid_user = User.authenticate(@user_login)
    if (valid_user)
      logger.info("user logged in: #{valid_user}")
      @session['user'] = valid
      redirect_to_path @return_to
    else
      @session['return_to'] = @return_to
      redirect_to :controller => 'rsvp', 
                  :action => 'select_invitation',   
                  :params => { 'name' => @params['user_login'] }
    end
  end

  def logout
    reset_session
    redirect_to :controller => 'information', :action => 'index'
  end
end

Both of these have, roughly, something to do with logging in. The rails controller is for an implementation of pass-through authentication very much like the Servlet spec's authentication system. The Stuts one for a classic link-to-the-login-page system. They work for our purpose. Interestingly, the Rails one doesn't use passwords (or even a stored login name, it makes sense when you see the app, don't worry).

Now, presuming you know how to make sense of the Struts one, lets look at the Ruby one. It does a few things differently. First off, all Rails controllers are by default much like the DispatchAction in Struts. Each method defined on the controller exposes an action to the application, so the above LoginController exposes four actions: index, pass_thru, login, and logout.

Form parameters in Struts are passed in via the ActionForm, in Rails you get a Hash (a Map) of values. Much like nested properties in Struts, you can have nested thingamabobs in Rails: @params['users']['bob']['name'] for example -- we'll look at that in more detail later.

Struts uses the return value to indicate a view, rails makes it a call:

render_action 'login'

And defaults to a view named the same as the action invoked (this is important to note as most actions just render their default view, this particular example is weird in that way).

Struts will map the possible responses with named forwards typically, some local to the action, some global. The same concept exists in Rails, but instead of using xml to map these, it uses the file system as a nice indexing system:

brianm@kite:~/Sites/wedding/app$ tree views/
views/
|-- gift
|   |-- _gift_table.rhtml
|   |-- edit.rhtml
|   |-- list.rhtml
|   |-- new.rhtml
|   `-- show.rhtml
|-- information
|   |-- gardens.rhtml
|   |-- index.rhtml
|   |-- museum.rhtml
|   `-- reception.rhtml
|-- layouts
|   `-- scaffold.rhtml
|-- login
|   |-- login.rhtml
|   |-- logout.rhtml
|   `-- register.rhtml
`-- rsvp
    |-- index.rhtml
    |-- register.rhtml
    |-- select_invitation.rhtml
    |-- thank_you.rhtml
    |-- unlisted.rhtml
    `-- verify_invitation.rhtml

5 directories, 19 files
brianm@kite:~/Sites/wedding/app$ 

There are several ways to specify a view to use. The default looks for the [controller]/[action] naming convention. You can also use a named one render "my_fancy_view" if you like, though this is pretty uncommon, I think. It would just look for a my_fancy_view.rhtml view.

Information which would be in the struts-config.xml about redirects, etc, is placed into the controller here, for example the complicated view selection:

      redirect_to :controller => 'rsvp', 
                  :action => 'select_invitation',   
                  :params => { 'name' => @params['user_login'] }

sends a redirect to a different action on a different controller (in this case the login failed with no user matching using a fairly sophisticated name to user mapping algorithm (ruby-lucene would be better, alas)) so the best guess is that it is an as-yet un-rsvp'd wedding guest, so it sends em over to rsvp, with a query tagged on to find invitations for the attempted user name.

I can see you turning queazy. It's okay. I am a little uncomfortable redirecting to a different controller, complete with params as well. Redirecting to the same controller is a comfortable level of familiarity, but hey, I took the code as i wrote it, for better or worse =) It nicely illustrates that actions are much more responsible for choosing views in Rails than in Struts

Now that I made you queazy, and apologized, go look for where you have done the same thing in XML for Struts. You should feel even queazier about that, imho. [soapbox]XML is not a scripting language[/soapbox].

The only thing left to look at in Part 1 is the header-y type stuff:

layout 'scaffold'
model :user
helper_attr :return_to, :user_password, :user_login

defined on the controller. These are all slightly different.

The model :user is a convenience declaration to say "make sure everything I need to work with users has been required". Fancy import (fancy as it does nice things like always reloads in devel mode, doesn't in production mode, etc).

The layout 'scaffold' is analogous to using Sitemesh, and we'll look at it in more detail in a later entry, but for now, think of it as declaring a Sitemesh decorator (it is, really).

Finally we have, helper_attr :return_to, :user_login. These are helpers, which we will also look at in much mroe detail later. This specific case exposes two properties to the view, return_to and user_login. It is like setting them as attributes on the HttpServletRequest so that you'll have them in your JSP. Actually, it is just like that -- you expose them to the view template. The values are taken from the instance variables @return_to and @user_login respectively, which we set up in the actions.

Okay, start of the whirlwind comparison tour. More to come! Bedtime now =)

1 writebacks [/src/java] permanent link