Roller Struts2 Migration

This page provides info relevant to the Roller struts2 migration effort which is starting as of Roller 4.0 and will end ??

Roller Struts2 Design

This section tries to breakdown the various ways in which Roller uses struts2 and should give you an idea of what tools and code are at your disposal for working with struts2 in Roller.

Important Files

A number of files are changing for with the transition from struts1 -> struts2 and this is a simple list of what files are important in the struts2 configuration and what they are used for.

  • web/WEB-INF/classes/struts.xml
    • This is the new struts2 configuration file and it defines the majority of the functionality of the struts2 framework including all of our interceptors, packages, and actions.
  • web/WEB-INF/classes/
    • This is a companion file to the struts.xml file and it provides settings for how struts2 should run, so things like what default theme to use, what extension our actions use, what object factory to use, etc. This file should rarely need to be changed.
  • web/WEB-INF/tiles.xml
    • This is the new tiles2 definition file and defines all of our tiles targets and their components. This works basically the same as the old tiles-config.xml file.
  • src/org/apache/roller/ui/authoring/struts2/editor-menu.xml
    • This is the xml configuration file which defines the tab structure for the "editor" tabbed menu. When you are defining actions which are part of that menu you may need to edit that file.
  • src/org/apache/roller/ui/admin/struts2/admin-menu.xml
    • Same as above but for the "admin" tabbed menu.
  • web/WEB-INF/jsps/*/struts2/
    • This is a wildcard, but you get the point. All of the new struts2 jsps are under their own "struts2" directory which separates them from the old struts1 jsps while we migrate. This is just a convention to help us stay organized.
  • src/*/struts2/
    • Same as above but for the source code. All code which is specifically related to running struts2 is placed somewhere in the src tree under a "struts2" directory so that it's easy to identify it's purpose.

UIAction class

One of the goals of the struts2 migration is to make our actions simpler and easier to work on by having all the code and features which are common to all (or most) actions take place automatically and the UIAction class is how we do that. The UIAction class is a base class for actions to extend and provides support for all the things that we expect most of our actions will need to do, such as looking up the authenticated user, applying security checks on weblog permissions, setting action messages and errors, defining a page title, setting the tabbed menu preferences, etc, etc. Many of these things which were handled in custom or inconsistent ways in our struts1 forms are now handled in a simple and consolidated manner now and are provided to action developers for free.

Feel free to take a look at the UIAction class when you first start working with struts2 actions in Roller, but i'll talk about some of it's features in the coming sections. The main thing to know is that the UIAction class is meant to serve as a solid base for all Roller struts2 actions.

All struts2 actions should extend the UIAction class unless you have a very specific reason not too.

Action Freebies

One of the main features of the UIAction class is that it provides free access to some of the things that were handled in custom ways in the old struts1 actions. The best examples of this are how to access the UserData object representing the authenticated user, and the WebsiteData object representing the weblog being dealt with by the action. Pretty much all of Roller's actions require these 2 objects to work properly and so the UIAction has taken care of them for you by providing you getAuthenticatedUser() and getActionWeblog() methods.

These objects are automatically populated for all actions which extend UIAction so that the writer of the action can simply expect them to be there and focus on the action logic rather than filling their action with lots of boiler plate code for checking for these objects and looking them up. These objects are populated by a special struts2 interceptor which is applied to all actions which extend UIAction, so effectively what you get as an action developer is a freebie =)

Currently these are the only 2 objects which are extracted from the request and made available via UIAction, all other objects should be loaded by the action itself since they are more action specific. Hint: read about the myPrepare() method below.

Security Enforcement

One of the major things that the old struts1 code did poorly was action security enforcement. In specific, in all of the old struts1 code you would see a bunch of boilerplate code which basically did this ...

RollerSession rses = RollerSession.getRollerSession(request);
RollerRequest rreq = RollerRequest.getRollerRequest(request);
UserData user = rses.getAuthenticatedUser();
WebsiteData weblog = rreq.getWebsite();
if(user.hasPermissions(weblog, PermissionsData.AUTHOR)) {
  // do action logic
} else {
  return mapping.findForward("access-denied");

Now that's a lot of code to be duplicating in every action and lucky for you, with the new struts2 actions it's no longer necessary. Instead of having all that security enforcement code inside of the action methods its now been pushed higher up the execution chain and it happens in a custom interceptor before the action method ever gets executed, so all you need to know is that if the request actually gets to your action method then all the right permission checking has been done already.

Now, obviously each action will have different security constraints and we still need a way to specify those constraints, so your job isn't completely done. All you need to do to ensure that the proper security rules are applied for your action is to override some security methods. The UIAction base class implements an interface called UISecurityEnforced which looks like this ...

    public boolean isUserRequired();
    public boolean isWeblogRequired();
    public String requiredUserRole();
    public short requiredWeblogPermissions();

These methods provide a very simple way for action writers to control the security enforcement on their action. By overriding any of these methods you can configure your action to not enforce any security rules at all or to enforce very specific security options, it's up to you. To set a security preference just implement the method as you would expect. So to enforce that users must have the ADMIN permission on the action weblog to use your action you would do this ...

    public boolean isUserRequired() { return true; }
    public boolean isWeblogRequired() { return true; }
    public String requiredUserRole() { return "editor"; }
    public short requiredWeblogPermissions() { return PermissionsData.ADMIN; }

That's it! If you put the code above in your action and are extending UIAction then your action will ensure that the logged in user has ADMIN permissions on the action weblog before executing. And, to make things even easier, the UIAction class is defined with some reasonable defaults so that you don't have to implement all of those methods in every action. By default the UIAction class enforces these rules ...

    public boolean isUserRequired() { return true; }
    public boolean isWeblogRequired() { return true; }
    public String requiredUserRole() { return "editor"; }
    public short requiredWeblogPermissions() { return -1; }

This means that by default, all actions require that a user and weblog is properly specified and that the user has the "editor" role. So now all you really need to do for the example we gave above is put this in your action to enforce the ADMIN permission ...

    public short requiredWeblogPermissions() { return PermissionsData.ADMIN; }

Pretty slick eh? Note: the reason why there is no default required weblog permission is mostly to alleviate any confusion about required permissions for actions. Basically, we want the action classes to define the requiredWeblogPermissions() method as shown above so that anyone working on the action can see directly in that class what the enforced permissions are.

So, now that we have some nice action freebies already set for us and we've defined the security for our action, lets look at the myPrepare() method to see how we can use that to easily do common preparation code that our action uses.

myPrepare() method

The myPrepare() method is basically a way for actions to do any setup work that applies to their action before the actual action method is called. This is particularly great for loading things from the database that you know your action methods will need. A good example of this from the ThemeEdit action is how the myPrepare() method loads the list of enabled themes for the action to use.

public void myPrepare() {
    ThemeManager themeMgr = RollerFactory.getRoller().getThemeManager();

That's it. Now I know that my action has already looked up the list of themes that are used by all the action methods defined in my action and the code is in a nice and reusable place. So if you have any work that you want to do just before your action method executes, then implement the myPrepare() method.

Tabbed Menu controls

One of the page elements that is required for pretty much all actions is the tabbed menu which displays near the top of the page and show the current location of the user. The old Roller code used to render that menu using a custom jsp taglib which was hard to understand and effectively used lots of custom request parsing code to figure out what page the request was on and what permissions to enforce, etc. All of that has been updated for the struts2 actions and design has changed to use a simple tiles jsp for rendering the tabbed menu and the logic for forming the menu has been pushed to the actions where it is most appropriate. Basically, with the new struts2 code it is an actions job to properly provide a menu to be rendered.

Now, as you would expect we have made the process for actions to provide a menu extremely simple. The UIAction class defines a few methods which deal with controlling and accessing the menu for that action via a getMenu() method. To define how to render a standard Roller tabbed menu you simply need to set 2 attributes on your action which are inherited from the UIAction class, the actionName and desiredMenu attributes. These 2 attributes tell the menuing code what action is currently selected and what menu should be rendered, the rest is handled for you by using a MenuHelper class to construct a Menu which is appropriate.

So, to specify these attributes you will simply want to define their values in the default constructor for your action like so ...

    public MyAction() {
        this.actionName = "myAction";
        this.desiredMenu = "editor"; // choose editor menu

NOTE: the actionName value must specify the name of your action as defined in the struts.xml action mapping, not necessarily your action class name. the desiredMenu attribute is just the name of the menu, which currently only has options for "editor" and "admin".

Exception and Error/Message Handling

One of the biggest changes with the struts2 migration is going to be how Roller handles errors and exceptions. The old struts1 code did a pretty poor job of properly handling exceptions and with the struts2 migration that is changing. There is really only one significant change happening, but it's a big one, with the new struts2 actions all actions must catch their own exceptions and deal with them somehow. It is no longer acceptable to define an action method as "public String execute() throws Exception".

Luckily, now that we are using struts2 it's a little bit easier to report errors on your action so hopefully that will make it easier and more encourageable for action writers to properly handle errors. To define an error on a struts2 action all you need to do is call the addError() method and you are done. The addError() method has 2 forms, one which simply accepts a bundle key, the other which accepts a bundle key and an optional parameter ...

    public void addError(String errorKey);
    public void addError(String errorKey, String param);

Same goes for message notifications as well ...

    public void addMessage(String msgKey);
    public void addMessage(String msgKey, String param);

So setting errors/messages to display on the UI is now as easy as it's every going to be and there are no more excuses for being lazy and simply throwing your exceptions out of your action method for something else to handle.

Migration Walkthrough

Assuming that you have read through and understand the basic design of how Roller uses struts2 and you are ready to try migrating some of the code, this walkthrough can serve as a decent starting point and helper to show you what you are getting into. Of course, this is only a best effort guide and each action will have it's own quirks and gotchas which may not be covered here. If that's the case then try to sort it out and if you can't then ask about it on the Roller dev list.

Migrating Actions

Here are some notes on the series of steps that you might run through to migrate an existing Roller struts1 action to a new struts2 action. The specific details of the work will vary depending on the situation.

  • copy old action code to new action class and rename (makes things less confusing)
  • make sure new package name is appropriate at the top of your copied code
  • have class extend UIAction
  • define security needs of your action.
    • remove all attempts to get authenticated user via RollerSession and replace with a simple call to getAuthenticatedUser(). access to the authenticated UserData object is provide for you by having your action extend the UIAction class, so there is nothing that you need to do. by default, all actions require an authenticated user, so if the user is not properly authenticated when trying to access an action then they will get an access denied page.
    • remove all attempts to get the weblog used by the action via a RollerRequest object and replace with a simple call to getActionWeblog(). just like above, this is extracted from the request and populated for you.
    • to see how you can control the security options for your action, read the Security Enforcement section above.
  • fix action method declarations
    • action methods now return just a String instead of an ActionForward
    • action methods now do not accept any params
    • action methods do not throw any exceptions (handle these inside your action method!)
  • fix action method return statements to just return a String instead of an ActionForward
  • fix up error/message handling
    • delete all instances of ActionMessages() and ActionErrors()
    • delete all calls to saveMessages() and saveErrors()
    • replace all attempts to set action messages/errors with calls to addMessage(key), addMessage(key, param), addError(key), and addError(key, param). these methods are convenience methods built into the UIAction class which make it trivial to set errors and messages which will automatically get rendered on the page.
  • fix up handling of submitted data
    • if your action handles a lot of fields then define a class level attribute called "bean" which will serve as a form bean for your action and provide a getter and setter method. check out an existing example like the RegisterForm for an example.
    • if your action only needs to make use of a couple fields then you can just define those attributes directly in your action and provide getters and setters.
    • NOTE: if you are defining a "bean" in your action, make sure that the bean gets initiated. i.e. you can't define your bean as "MyBean bean = null" because that's not an initiated attribute. instead you should probably do this ... "MyBean bean = new MyBean()"
    • once that is done you need to change usages of the old calls to getBean().xxx() or just xxx() if you didn't define a "bean".
    • you can also delete all old code which trys to extract the form bean from the passed in actionForm.
  • fix up form validation
    • if there is a custom validate() method then replace it with a no arg method myValidate(). it is very important that you don't leave a validate() method in your action because that is a struts2 specific method and will almost certainly cause behavior you didn't intend.
  • tidy up the action results.
    • try to use the defaults like SUCCESS and INPUT, and custom ones where applicable, like "cancel".
  • fix up use of "models"
    • models are no longer used with our struts2 actions. in struts2 your action is your model, so anything that was being used through a model class needs to be transferred into your action class. be selective here though, the old struts1 code provided lots of model methods which weren't really necessary.

Migrating JSPs

  • replace include for taglibs.jsp to taglibs-struts2.jsp
  • do a search and replace for "fmt:message key" with "s:text name"
  • replace html:form with s:form and delete the method param
  • change the new s:form action attribute to the correct action name, like myAction!method
  • replace instances of "html:text property" with "s:textfield name"
  • add "bean." in front of all name attributes for form fields if you are using a "bean".

Migration Progress

Core Actions

  • Login - migrated
  • Register - migrated
  • Profile Update - migrated
  • Create Weblog - migrated
  • Main Menu - migrated

Authoring Actions

  • Entry Add - migrated
  • Entry Edit - migrated
  • Entry Remove - migrated
  • Entries - migrated
  • Comments - migrated
  • Categories - migrated
  • Category Add - migrated
  • Category Edit - migrated
  • Category Remove - migrated
  • Bookmarks - migrated
  • Bookmark Add - migrated
  • Bookmark Edit - migrated
  • Folder Add - migrated
  • Folder Edit - migrated
  • Resources - migrated
  • Referrers
  • Weblog Settings - migrated
  • Weblog Remove - migrated
  • Theme Edit - migrated (mostly)
  • Templates List - migrated
  • Template Add - migrated
  • Template Edit - migrated
  • Template Remove - migrated
  • Pings - migrated
  • Custom Ping Targets - migrated
  • Maintenance - migrated

Admin Actions

  • Global Config - migrated
  • User Admin - migrated
  • Global Comment Management - migrated (mostly)
  • Global Ping Targets - migrated
  • Cache Info - migrated

Planet Actions

  • Planet Config
  • Planet Subscription
  • Planet Group
  • No labels