10.30.2008

Is JSF Fixable?

Subbu says that JSF is Not Fixable. His he right or his he wrong? That is the question!

I've been wanting to write about JSF for a while now. I was an early adopter and big proponent of JSF when it first appear at the surface of Java land. Since then, I've worked on many projects that used this framework and slowly but surely, my opinion changed about this framework.

In Subbu opinion, JSF is not fixable because it was not designed with web architecture in mind. The framework is not RESTful and requires POST for everything. JSP proponent argue that RESTful is coming to JSF and it is even already supported right now when you use JBoss Seam.

It's true, that JSF is getting better. Facelets will become part of JSF (it should have been there from day though - why make a brand new framework and use broken things like JSPs). JSF 2.0 will make some space for GETs and JBoss Seam fixes a lot of other issues with JSF.

However, if JSF is not Fixable, it is not solely because it is not RESTful. JSF is broken at many places.

The JSF request processing life-cycle is one of the worst thing about JSF. There is so many steps in the life-cycle, different ways to exit the normal processing and this gives something many entry points for custom components (one for each steps in the life-cycle). Things can go wrong at many places without even touching your application code.

Closely related to the request processing life-cycle, is the component model. In JSF, components are a kind of black box. But when you think that black box should be mostly independent on the framework (beside the connecting part with the framework), there is so many implementation leak in JSF, that your component must be ported to every JSF implementation. I say ported and not tested... Because the order in which the component tree is processed can make you component fail. You have to make so many assumption based on the JSF implementation (things that are not present in the specification) that you actually need to choose your JSF implementation carefully and will never be guaranteed that your component will survive even minor updates in the JSF implementation.

Want to add to functionality to an existing component? There is no easy way to do that. The best way, is to take the source code (if available) for the component and refactor it with some new name and then hack you way through it. And when I say hack, I mean it in a bad way! I've never saw a component source code that was easily readable and understandable. Large component sets like ADF Faces are notably ugly. Even the Swing component model (which is quite complex) is super simple compared to JSF component model.

It looks like in JSF, the worst thing about leaking abstractions made it into the framework and the good thing about encapsulation where left out.

And before I get flamed that you will be able, with JSF 2.0 - which is still in draft, to create composite components soon... I have to respond that soon is not now.

One of a good indication that JSF is broken beyond fixable, is that you cannot do simple thing simply. Want to create a new component, then you have something like 3 files to edit. Want to create a new converter, again, three files needs to be edited. I know that good thing came in sets of three, but this is not the kind of good I want.

Another good indication that JSF is broken beyond fixable, is when thing go wrong. Good luck finding you problem... Facelets help you a bit by giving you a more precise location of the issue. But it does not always work. When you look at stack trace, you have to scroll hundreds of line before you see the line from you application. But what the hell do you do when you don't even have any of you application code in the stacktrace? Why on earth, you are have a stacktrace that is 30-40 level deep before even reaching an action method? Just to process a fringing POST.

In JSF, the framework is everywhere except where you need it.

Yes, there is some good things about JSF, I like the expression language (even is it is a bit inconsistent and not powerful enough to my taste). But the bad things are so deeply rooted. If you are just a user of the framework and that you are writing really simple applications, then it does look like JSF is great, but whenever you start digging a bit and adding a bit of complexity (Ajax anyone? Without another component set that takes a few MB?), the framework start eating up your nerves.

So would I ever recommend JSF again? Maybe maybe not, in the Java world, there is so many framework to choose from, that your cannot recommend one with the certainty that it will still be around one or two years from now. The only sure thing, is that JSF will be there because it is the standard... And this is very sad!

10.20.2008

Making Mr Bones work with jRuby

UPDATE : The fixes outlined bellow where incorporated by Tim in the main Mr Bones distribution. If you update to the latest gem, it will work out of the box with jRuby.

Want to make some GEM, Mr Bones (github) is going to help you setup your directory structure and comes with Rake tasks to package your application into a nice little Gem. The great thing about Mr Bones is that it does not add any dependencies to your Gem (like hoe does).


To create a new project, you just type bones [project_name] and you can start developping your application or library. You are done? type bones gem:install and your code is packaged and installed.


Want to do the same using jRuby instead of plain old Ruby (MRI)... You do just the same... But as of version 2.1.0, all you would just get errors when using Bones. The reason is that Mr Bones does things slightly differently dependings on the RUBY_PLATFORM value.

For example, if Bones detected that the platform was win32, it does it's redirection to nul: instead of /dev/null.

The problem with running Bones under jRuby, is that there is no win32 string in the value of RUBY_PLATFORM. Instead, you have java. So if you run Bones with jRuby under Windows, it does not beleive it's running under Windows and it does thing like it would if running under Unixes.

I really wanted Bones to work under jRuby, so I started working on a patch... Here is what I've done...

In lib/bones.rb, the existing code set a WIN32 constant to true if RUBY_PLATFORM contains win32. It then uses this constant to set DEV_NULL constant to the proper null device for redirection. I believe a more appropriate way of doing this is to check for the existence of /dev/null and then set DEV_NULL depending on the result. So on all Unix based OSes, the value would be /dev/null because it exists, else it would be nul: (these days, only Windows is not Unix based).

I also set a HOME constant that will point to the user home directory. To do this, I assign the value of the HOME environment variable and if its not set, I use the value of the USERPROFILE environment variable. Again, no need to check the platform, just check for things that should be there one way or another.

Finally, Bones will run things like rake or gem. It does so by using the system(...) method or Rake sh(...) method. This is fine if you have only one installation of Ruby on your system. But if you have many installation like I does, it will use the one it find in the PATH. So I use the same trick as Rake to find the ruby intepreter we are running on and set it to the RUBY constant. We will use this later on to make sure we use rake or gem for the same interpreter.

Here is the patch for lib/bones.rb :
diff --git a/lib/bones.rb b/lib/bones.rb
index 50a63b6..35dcd75 100644
--- a/lib/bones.rb
+++ b/lib/bones.rb
@@ -4,8 +4,9 @@ module Bones
   # :stopdoc:
   VERSION = '2.1.1'
   PATH = File.expand_path(File.join(File.dirname(__FILE__), '..'))
-  WIN32 = %r/win32/ =~ RUBY_PLATFORM
-  DEV_NULL = WIN32 ? 'NUL:' : '/dev/null'
+  HOME = File.expand_path(ENV['HOME'] || ENV['USERPROFILE'])
+  DEV_NULL = File.exist?("/dev/null") ? "/dev/null" : "nul:"
+  RUBY = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']). sub(/.*\s.*/m, '"\&"') # Ruby Interp
reter location - taken from Rake source code
   # :startdoc:

   # Returns the path for Mr Bones. If any arguments are given,
@@ -16,6 +17,7 @@ module Bones
     args.empty? ? PATH : File.join(PATH, args.flatten)
   end

+
   # call-seq:
   #    Bones.require_all_libs_relative_to( filename, directory = nil )
   #
@@ -46,7 +48,7 @@ module Bones
     load bones_setup

     rakefiles = Dir.glob(File.join(Dir.pwd, %w[tasks *.rake])).sort
-    import(*rakefiles)
+    import(*rakefiles) unless rakefiles.empty?
   end

   # TODO: fix file lists for Test::Unit and RSpec


Now onto lib/bones/main.rb. In main.rb, there are calls to rake. This is one place I use the RUBY constant mentionned earlier to execute rake from the same interpreter location and not from the system PATH. To do that, I just do
system("#{::Bones::RUBY} -S rake ...") instead of just system("rake ...")

Ther was also a place in this file that the old WIN32 constant was used to find the user home directory. So I replace this with the HOME constant we talked a bit earlier.

Here is the patch for lib/bones/main.rb :
diff --git a/lib/bones/main.rb b/lib/bones/main.rb
index b7ec28b..1cc074a 100644
--- a/lib/bones/main.rb
+++ b/lib/bones/main.rb
@@ -181,9 +181,9 @@ class Main
       if test(?f, File.join(output_dir, 'Rakefile'))
         begin
           FileUtils.cd output_dir
-          system "rake manifest:create 2>&1 > #{::Bones::DEV_NULL}"
+          system "#{::Bones::RUBY} -S rake manifest:create 2>&1 > #{::Bones::DEV_NULL}"
           @io.puts "now you need to fix these files"
-          system "rake notes"
+          system "#{::Bones::RUBY} -S rake notes"
         ensure
           FileUtils.cd pwd
         end
@@ -363,7 +363,7 @@ class Main
   def mrbones_dir
     return @mrbones_dir if defined? @mrbones_dir

-    path = (::Bones::WIN32 ? ENV['HOMEPATH'].tr("\\", "/") : ENV['HOME'])
+    path = ::Bones::HOME
     path = File.join(path, '.mrbones')
     @mrbones_dir = File.expand_path(path)
   end

It was now lib/bones/tasks/setup.rb b/lib/bones/tasks/setup.rb turn to get dissected. There, it was again seting the value of a WIN32 constant with a slightly different logic than in bones.rb. It used this constant to set DEV_NULL again and then to use diff.exe instead of just diff when running under Windows. I removed the WIN32 constant. Got rid of the if used to add the ".exe" extension as they are not necessary. If ther is a diff.exe on Windows, system("diff ...") will work just nice.  When checking for an appropriate diff utility, the existing code does some system commands inside a quiet context. the quiet(...) method just changed the stderr and stdout to the null device before running the block. There is a problem when calling the reopen() method under jRuby that made the quite method effect stay there until the end of the program. So I removed that method and used redirection in the system(...) call to acheive the same result.

I then set the value of the RCOV, RDOC and GEM constant to "#{RUBY} -S rcov", ... so that these commands execute under the same interpreter we are currently running.

Here is the lib/bones/tasks/setup.rb b/lib/bones/tasks/setup.rb patch :
diff --git a/lib/bones/tasks/setup.rb b/lib/bones/tasks/setup.rb
index 27d659b..a722c13 100644
--- a/lib/bones/tasks/setup.rb
+++ b/lib/bones/tasks/setup.rb
@@ -5,7 +5,7 @@ require 'rake/clean'
 require 'fileutils'
 require 'ostruct'

-class OpenStruct; undef :gem; end
+class OpenStruct; undef :gem if defined? gem; end

 # TODO: make my own openstruct type object that includes descriptions
 # TODO: use the descriptions to output help on the available bones options
@@ -123,36 +123,17 @@ import(*rakefiles)
 %w(lib ext).each {|dir| PROJ.libs << dir if test ?d, dir}

 # Setup some constants
-WIN32 = %r/djgpp|(cyg|ms|bcc)win|mingw/ =~ RUBY_PLATFORM unless defined? WIN32
-
-DEV_NULL = WIN32 ? 'NUL:' : '/dev/null'
-
-def quiet( &block )
-  io = [STDOUT.dup, STDERR.dup]
-  STDOUT.reopen DEV_NULL
-  STDERR.reopen DEV_NULL
-  block.call
-ensure
-  STDOUT.reopen io.first
-  STDERR.reopen io.last
-  $stdout, $stderr = STDOUT, STDERR
-end
+DEV_NULL = File.exist?("/dev/null") ? "/dev/null" : "nul:"
+
+DIFF = if system("gdiff '#{__FILE__}' '#{__FILE__}' > #{DEV_NULL} 2>&1") then 'gdiff'
+       else 'diff' end unless defined? DIFF
+
+SUDO = if system("which sudo > #{DEV_NULL} 2>&1") then 'sudo'
+       else '' end

-DIFF = if WIN32 then 'diff.exe'
-       else
-         if quiet {system "gdiff", __FILE__, __FILE__} then 'gdiff'
-         else 'diff' end
-       end unless defined? DIFF
-
-SUDO = if WIN32 then ''
-       else
-         if quiet {system 'which sudo'} then 'sudo'
-         else '' end
-       end
-
-RCOV = WIN32 ? 'rcov.bat' : 'rcov'
-RDOC = WIN32 ? 'rdoc.bat' : 'rdoc'
-GEM  = WIN32 ? 'gem.bat'  : 'gem'
+RCOV = "#{RUBY} -S rcov"
+RDOC = "#{RUBY} -S rdoc"
+GEM  = "#{RUBY} -S gem"

 %w(rcov spec/rake/spectask rubyforge bones facets/ansicode).each do |lib|
   begin

There is one last change that should be done to be able to run the specifications, we need to require 'rubygems' so the trick we took from Rake to get the current ruby interpreter work :
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5a60dc1..1965e1c 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -3,6 +3,7 @@
 unless defined? BONES_SPEC_HELPER
 BONES_SPEC_HELPER = true

+require 'rubygems'
 require File.expand_path(
     File.join(File.dirname(__FILE__), %w[.. lib bones]))

--


We can nw enjoy Bones with jRuby on Windows just like we would enjoy it under MRI.

I've submitted this patch to Tim, the auther of Mr Bones. While he review it, you can download it from here and apply it to your source tree to check it out.


AdSense Links