programming

Different content in Rails based on UserAgent

I was recently working on a website built using Rails that needed to render different content for certain user agents. Specifically, we needed simpler versions of certain pages for BlackBerry devices. Here's how I accomplished.

First, I added a new mime-type for BlackBerry by adding the following line to config/initializers/mime_types.rb:

Mime::Type.register_alias "text/html", :blackberry

Next, I added two utility methods to app/controllers/application.rb:

# Checks UserAgent
def is_blackberry?
  ua = request.user_agent
  return false if ua.nil?
  return false if ! ua.downcase.index('blackberry')
 
  # Don't call the BlackBerry 9800 a BlackBerry, since it has a modern browser
  # based on WebKit:
  # Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, Like Gecko) Version/6.0.0.141 Mobile Safari/534.1+
  return false if ua.downcase.index('webkit')
 
  # Must be a BlackBerry!
  true
end
 
# Sets the respond_to format to blackberry if blackberry
def set_blackberry_format
  if !request.xhr? && is_blackberry?
    request.format = :blackberry
  end
end

With that in hand, it's easy to render BlackBerry specific content on specific pages:

set_blackberry_format
respond_to do |format|
  format.blackberry
  format.html
  format.js { render :layout => false }
end

Ruby mixin for the Enum pattern

Sometimes you just want to use an Enum. Unfortunately, if you're a Ruby developer, Ruby does not offer a native enum structure. Here's a simple approach using a mixin module:

module Enum
  def const_missing(key)
    @enum_hash[key]
  end
 
  def add_enum(key, value)
    @enum_hash ||= {}
    @enum_hash[key] = NameValuePair.new(value, key.to_s.downcase)
  end
 
  def each
    @enum_hash.each {|key, value| yield(key, value) }
  end
 
  def enums
    @enum_hash.keys
  end
 
  def enum_values
    @enum_hash.values
  end
 
  def get_enum_hash
    @enum_hash
  end
 
  def find_by_key(key)
    @enum_hash[key.upcase.to_sym]
  end
end

The Enum mixin depends on a NameValuePair class to hold the data:

class NameValuePair
  attr_reader :label, :value
 
  def initialize(label, value)
    @label = label
    @value = value
  end
 
  def first
    @label
  end
 
  def last
    @value
  end
end

I included first and last methods to better support the select and options_for_select helper methods in Rails. Here's how you might use it:

class FooEnum
  extend Enum
 
   self.add_enum(:APPLE, "Apple")
   self.add_enum(:PEAR, "Pear")
   self.add_enum(:ALL, "All Fruit")
end
 
FooEnum::APPLE ==> #<NameValuePair @value="apple", @label="Apple">
FooEnum::ALL.value ==> "all"
FooEnum::ALL.label ==> "All Fruit"
FooEnum.find_by_key('apple') ==> #<NameValuePair @value="apple", @label="Apple">

To Rewrite or Not to Rewrite: The Ugly Question

I recently had a discussion about the idea of rewriting software from scratch. I actually played the devil's advocate and argued against ever throwing out and rewriting, which really got me thinking about the whole concept.

The discussion centered around article by Joel Spolsky (of Joel on Software) titled Things You Should Never Do, Part 1. His points against rewrites include:

  1. The ugly code you throw out has been hardened and tested. It's filled with bug fixes. You're throwing out that knowledge and expertise.
  2. You're throwing out market leadership and "giving a gift of two or three years to your competitors".
  3. You're not going to do a better job writing the code a second time than you did the first time, especially since it's unlikely you have the same team that wrote the earlier version.
  4. You will introduce new bugs.

Joel further argues that there are three major reasons developers want to rewrite code and none of them require rewrites:

  1. Architectural problems. The "you got your gui in my business logic" problem. This can be handled by small but steady code refactorings.
  2. Inefficiency. Again, can be handled by small code refactorings.
  3. The code is fugly. This may be due to complexity and bug fixes, in which case see point #1 above. Or it may be due to poor and changing naming conventions, in which case it can be fixed by a simple Find-Replace.

These are all excellent points. On some level, I agree with this entirely. Even many nasty combinations of all three problems can be solved by steady refactorings. I have worked for places where people pushed for rewrites that weren't necessary. But these were larger businesses with a well established core product. These were not early startups. That's why I believe Joel makes several assumptions which are fatal to his arguments.

First, he assumes the software project is really large and complex. While some of us may have worked on projects of that size and scope, quite of few of us work on much smaller projects. Simply put, it's a matter of scale.

Second, as a corollary of his first assumption, Joel also assumes that a rewrite requires years not months. Again, this is likely true for a product like Excel or Word... but this simply isn't true for many of the sites and products I've worked on. Furthermore, the use of agile or rapid development technologies such as Ruby on Rails can dramatically shrink this window.

Third, and perhaps most importantly, Joel assumes that the time required to cope with a messy code-base and make steady refactorings is significantly less than the time required to rewrite the app. And he assumes that's a worthwhile trade off. This may be clear cut for larger products or companies, but I question whether or not that's accurate for a startup. The more tangled your code, the longer it takes you to make changes. The longer it takes to make changes, the less nimble you are and the longer it takes you to respond to changes in company direction or marketplace demands.

It's that last point that I believe is most important to those of us working for small startups. We tend to be small young companies who are still striving to find our exact place in the wider world. We're often in cutting edge spaces where there is no clear cut path to success. And usually we're steadily seeing greater numbers of competitors in our space. It seems to me that agility is vitally important to people us. We need to be able to makes changes rapidly as our knowledge of the space evolves. Fundamentally, I think it's better to have a decent product/feature/whatever out in the hands of consumers than it is to have a nearly perfect product that's still under development. Don't get me wrong, I'm sure I'm preaching to the choir. :) But, I think it's critical to keep the need for agility and nimbleness in the forefront of our thoughts.

Fourth, Joel assumes any architectural problems can be solved by steady refactoring. Frankly, I disagree. I think there exist serious architectural flaws, especially related to scalability that cannot be easily solved by refactoring. eBay, LinkedIn, Facebook and Yahoo have all had major rewrites in their history that were directly attributed to serious architectural failings.

That is not to say that a full rewrite is necessarily a desirable goal. :) However, it takes careful management and planning to avoid finding yourself in this position. eBay used to employ a strategy they called headroom, which basically set aside 20+% of all development time to refactor code and it keep it in top working order. While I think it may very difficult to employ such a strategy in a startup, it may be worth considering.

FlashVars broken in IE8

Unsurprisingly, Internet Explorer 8 broke yet another feature of the web. This time, the folks at Redmond broke how Internet Explorer passes the flashvars parameter into Flash. Typically, when placing a Flash object on an HTML page, you use the following syntax:

<object id="flv" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" width="692" height="516" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" align="middle">
 <param name="allowScriptAccess" value="sameDomain" />
 <param name="allowFullScreen" value="true" />
 <param name="quality" value="high" />
 <param name="bgcolor" value="#424242" />
 <param name="FlashVars" value="name=value" />
 <param name="wmode" value="transparent" />
 <embed type="application/x-shockwave-flash" width="692" height="516" src="http://www.example.com/mysample.swf" name="flv" allowfullscreen="true" allowscriptaccess="sameDomain" wmode="transparent" bgcolor="#424242" quality="high" flashvars="name=value" pluginspage="http://www.macromedia.com/go/getflashplayer" align="middle"></embed>
 <param name="movie" value="http://www.example.com/mysample.swf" />
</object>

The OBJECT tag is used by Internet Explorer and the EMBED tag is used by everyone else (Firefox, Safari, etc.). Although the flashvars paramter is not formally described in the HTML 4.0.1 spec1, this code worked fine with IE 6 and IE 7. Unfortunately, IE 8 does not pass flashvars into the Flash Player. The only work around is to pass the flashvars parameter as part of the movie name parameter, as shown below:

<object id="flv" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" width="692" height="516" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" align="middle">
 <param name="allowScriptAccess" value="sameDomain" />
 <param name="allowFullScreen" value="true" />
 <param name="quality" value="high" />
 <param name="bgcolor" value="#424242" />
 <param name="FlashVars" value="name=value" />
 <param name="wmode" value="transparent" />
 <embed type="application/x-shockwave-flash" width="692" height="516" src="http://www.example.com/mysample.swf" name="flv" allowfullscreen="true" allowscriptaccess="sameDomain" wmode="transparent" bgcolor="#424242" quality="high" flashvars="name=value" pluginspage="http://www.macromedia.com/go/getflashplayer" align="middle"></embed>
 <param name="movie" value="http://www.example.com/mysample.swf?name=value" />
</object>

 

Notes:

1 http://www.w3.org/TR/html4/struct/objects.html#h-13.3.2

Embedding HTML Document in an IFRAME with GWT

While working on a recent project in GWT, I needed to embed a full HTML document inside an IFRAME. And I didn't want to specify a remote URL for the IFRAME - I actually wanted to shove the HTML content directly into the IFRAME.

Initially I thought it was trivial: create an IFrameElement and call setInnerHTML.

IFrameElement iframe = Document.get().createIFrameElement();
iframe.setInnerHTML(htmlContent);

Unfortunately, that doesn't work. It doesn't cause any errors, but it doesn't actually fill in the iframe. Instead, you have to use native javascript to write into the document object for the iframe:

private final native void fillIframe(IFrameElement iframe, String content) /*-{
  var doc = iframe.document;
 
  if(iframe.contentDocument)
    doc = iframe.contentDocument; // For NS6
  else if(iframe.contentWindow)
    doc = iframe.contentWindow.document; // For IE5.5 and IE6
 
   // Put the content in the iframe
  doc.open();
  doc.writeln(content);
  doc.close();
}-*/;

Voila! However, one of the side effects is that the iframe doesn't include any CSS unless your embedded HTML references a stylesheet. If you want to manually add a reference to a specific stylesheet, you can do that through native javascript as well:

private final native void addHeadElement(IFrameElement iframe, String cssUrl) /*-{
  setTimeout(function() {
 
    var body;
    if ( iframe.contentDocument ) { 
      // FF
      iframe.contentDocument.designMode= "On";
      iframe.contentDocument.execCommand('styleWithCSS',false,'false');
      body= iframe.contentDocument.body;
    }
    else if ( iframe.contentWindow ) {
      // IE
      body = iframe.contentWindow.document.body;  
    }
 
    if (body == null) {
      return;
    }
 
    body.className = "custom-body-classname";
    var head = body.previousSibling;
    if(head == null) {
      head = iframe.contentWindow.document.createElement("head");
 
      iframe.contentWindow.document.childNodes[0].insertBefore(head, body);
    }
    var fileref = iframe.contentWindow.document.createElement("link");
    fileref.setAttribute("rel", "stylesheet");
    fileref.setAttribute("type", "text/css");
    fileref.setAttribute("href", cssUrl); 
    head.appendChild(fileref);
  }, 50);
}-*/;}

There's still one more problem. You can't use either of native methods until the IFRAME element has been attached to the DOM. The easiest way around this is to add the IFRAME element to a panel and over the onLoad() method for the panel:

final IFrameElement iframe = Document.get().createIFrameElement();
FlowPanel innerBox = new FlowPanel() {
    @Override
    protected void onLoad() {
        super.onLoad();
 
        // Fill the IFrame with the content html
        fillIframe(iframe, contentHtml);
 
        // Add a HEAD element to the IFrame with the appropriate CSS
        addHeadElement(iframe, cssUrl);
    }
};
innerBox.getElement().appendChild(iframe);

I got the idea and the important code from an article titled Inject HTML into an IFrame from Software As She's Developed.

Syndicate content