July 22, 2009

Why Previous Java Web Framework Have Failed and Why Apache Wicket Will Succeed.

There exists numerous Java web framework (e.g. Struts, JSF, Spring MVC, etc.) and most of them are build upon the Model-View-Controller pattern, but though they have been used in numerous successful project, the Java community still produces new Java web framework. Why is that? According to me the previous Java web framework have several flaws:
  • The JSP technique is messy and clutters down the view page, maybe not for simple pages, but definitely for more complex pages.
  • The round trip when developing JSP pages is long since JSP pages can not be viewed, without first compiling them and running them in a web container. This severely slows down the project . This is especially true when one is fixing layout issues.
  • Web and JSP developer have different code base. This also slows down the project productivity, since when the web team ships new code or fixes, the JSP developer must copy past that into different JSP code.
  • Verbose XML configuration, is something the entire Java community is trying to get away from via Annotation and better default values. For example look at the EJB or Spring developing.
  • It is hard and verbose to unit test and even harder to maintain.
Though these many flaws there has not been any good alternative around to develop Java web solution, but now I would say the missing link is found – Apache Wicket. The Apache Wicket architecture differs in two major way.
  • It does not use JSP technology for coding Views, instead it uses simple HTML and extend it with attributes.
  • You create components (TextField, CheckBox, etc.) in the Controller and add behavioral via Listener classes, such as in Swing.
These two major differences make a huge different:
  • Web and View developer use the same code base.
  • No need to compile and run the View in a container, simply open the View in a web browser and watch differences.
  • Easy to write unit test code.
Before going through an example, lets first take a brief overview over the main Wicket classes. Component are simple Java classes with supporting HTML markup. Each Components holds a Model that is a simple Java class with data. What is here imported is that the default Model are stateful and most example for Wicket uses the default model. This is unfortunate because that is not best practice, because stateful objects consume memory and make the application less scalable. The two most imported classes two use to make the application stateless is
      IModel usersModel = new LoadableDetachableModel() {
          protected Object load() {
              return WicketApplication.get().getUserService().getUsers();
          }
      };
and
      new BookmarkablePageLink(“newUser”, UserEditPage.class);
and if you need to pass data use Link with as little data as possible.
              Link view = new Link("editUser", itemList.getModel()) {
                  public void onClick() {
                      setResponsePage(new UserEditPage(user.getUserId()));
                  }
              };
Now look how easy and clean the unit test is:
public class UserEditPageTest extends TestCase {
 protected static final Logger log = Logger.getLogger(UserEditPageTest.class);
 private WicketTester tester;

 public void setUp() {
  tester = new WicketTester(new WicketApplication());
 }

 private void setUpNewUserEditPage() {
  tester.startPage(UserEditPage.class);
  tester.assertRenderedPage(UserEditPage.class);
 }

 public void testLayout() {
  setUpNewUserEditPage();

  tester.assertComponent(UserEditPage.FEEDBACK_PANEL, FeedbackPanel.class);
  tester.assertComponent(UserEditPage.USER_FORM, Form.class);
 }

 public void testNewUser() {
  setUpNewUserEditPage();

  FormTester form = tester.newFormTester(UserEditPage.USER_FORM);
  form.setValue(UserEditPage.USERNAME, "kalle.stropp@fake.com");
  form.setValue(UserEditPage.PASSWORD, "validpassword");
  form.select(UserEditPage.ROLES, 0);
  form.submit(FormBuilder.BUTTON_SAVE);

  tester.assertRenderedPage(UserListPage.class);
  tester.assertNoErrorMessage();
 }
}
And here follows the rest of the code:
public class UserEditPage extends TemplatePage implements ButtonListener, IChoiceRenderer {
 private static final long serialVersionUID = 1L;
 protected static final String FEEDBACK_PANEL = "feedbackPanel";
 protected static final String USER_FORM = "userForm";
 protected static final String USER_ID = "userId";
 protected static final String USERNAME = "username";
 protected static final String PASSWORD = "password";
 protected static final String ROLES = "roles";
 protected static final Logger log = Logger.getLogger(UserEditPage.class);

 public UserEditPage() {
     setModel(new CompoundPropertyModel(new LoadableDetachableModel() {
   private static final long serialVersionUID = 1L;

   protected Object load() {
             return new User();
         }
     }));
     init(false);
 }

 public UserEditPage(final long userId) {
     setModel(new CompoundPropertyModel(new LoadableDetachableModel() {
      private static final long serialVersionUID = 1L;
      
         protected Object load() {
             User user =  WicketApplication.get().getUserService().loadUser(userId);
             user.setPassword(null); // hide current password
             return user;
         }
     }));
     init(true);
 }
 
   protected String getPageTitle() {
    return "userEditPage.title";
   }
 
 public void init(boolean readOnly) {
  add(new FeedbackPanel(FEEDBACK_PANEL));
  add(createForm(readOnly));
 }

 public Form createForm(boolean readOnly) {
  FormBuilder builder = new FormBuilder(USER_FORM, getModel());
  builder.addLabel(USER_ID);
  builder.addTextField(USERNAME, true, 30, readOnly);
  builder.addPasswordTextField(PASSWORD);
  User user = (User) builder.getModelObject();
  builder.addDropDownChoice(ROLES, Arrays.asList(user.getRoles().getValues()), this);
  builder.addSubmitButton(this);
  return builder.create();
 }

 @Override
 public void onSubmit(Object modelObject) {  
  log.info("onSubmit " + modelObject);
  WicketApplication.get().getUserService().storeUser((User) modelObject);
       setResponsePage(UserListPage.class);  
 }

 @Override
 public Object getDisplayValue(Object object) {
       return ((Roles) object).getRole();
 }

 @Override
 public String getIdValue(Object object, int index) {
  return ((Roles) object).getRole();
 }
}
And the HTML
 <html >
 <head > </head >
 <body >

 <wicket:extend >
 
  <div wicket:id="feedbackPanel" >Feedback Panel </div > 
  
  <form wicket:id="userForm" >
  <fieldset style="width:250px;" >
   <legend > <wicket:message key="userEditPage.userForm" >User Form </wicket:message > </legend > 
   <table >
    <tr >
     <td align="right" > <wicket:message key="userEditPage.userId" >UserId </wicket:message >: </td >
     <td > <span wicket:id="userId" id="userId" >1 </span > </td >
    </tr >
    <tr >
     <td align="right" > <wicket:message key="userEditPage.username" > <u >U </u >sername </wicket:message >: </td >
     <td > <input wicket:id="username" type="text" name="username" accesskey="u" / > </td >
    </tr >
    <tr >
     <td align="right" > <wicket:message key="userEditPage.newPassword" >New  <u >p </u >assword </wicket:message >: </td >
     <td > <input wicket:id="password" type="password" name="password" accesskey="p" / > </td >
    </tr >
    <tr >
     <td align="right" > <wicket:message key="userEditPage.roles" > <u >R </u >oles </wicket:message >: </td >
     <td > <select wicket:id="roles" > </select > </td >
    </tr >   
    <tr >
     <td colspan="2" align="right" >     
      <button wicket:id="save" type="submit" accesskey="s" > <wicket:message key="userEditPage.save" > <u >S </u >ave </wicket:message > </button >
     </td >
    </tr >  
   </table >
  </fieldset >
     </form >

 </wicket:extend >

 </body >
 </html >

No comments: