BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Migrating to Struts 2 - Part II

Migrating to Struts 2 - Part II

This item in japanese

Bookmarks

In the first part of this series, we explained (for Struts developers) the high level architecture, basic request workflow, configuration semantics and differences in the action framework in the new Struts 2 (formerly WebWork) and Struts 1. Armed with this knowledge, migrating an application of any size from Struts to Struts 2 should be simplified.

In this part of the series we are going to focus on converting the actions. Before delving into the code changes that are required, we first need to set the scene. We will discuss the example application that is to be converted, and the common components that are used in both Struts and Struts2 versions.

From there, we will review the Struts application code to see what it looks like when converted to Struts2 code. To wrap up, we'll take a look at the configuration changes.

The Example Application

To keep things simple we are going to choose an example that most people should be familiar with - a weblog. Although simple and perhaps somewhat overused (perhaps not quite as much as the Sun Pet Store), it is an example that doesn't require explaining.

To more concisely define the features, the use cases we will be discussing are:

  1. Add a new weblog entry
  2. View a weblog entry
  3. Edit a weblog entry
  4. Remove a weblog entry
  5. List all the weblog entries

Breaking it down further, the features we want to implement are those most common to web applications. They consist of create, read, update and delete - more commonly referred to as CRUD. Making these steps easy will greatly increase productivity.

There will also be a common component between the Struts and Struts2 applications, a back-end business service. Here's what it looks like:

public class BlogService {
private static List<Blog> blogs = new ArrayList<Blog>();
public List<Blog> list() { ... }
public Blog create(Blog blog) { ... }
public void update(Blog blog) { ... }
public void delete(int id) { ... }
public Blog findById(int id) { ... }
}

This object will support the use cases in our example. To simplify implementation we will instantiate this object in the action for both Struts and Struts2. This would provide unnecessary coupling in a real application, but for our example focusing on the web layer it is sufficient.

SIDEBAR: In Part I we discussed the interface injection style of dependency injection used in the Struts2 actions. This is the primary style used for the injection of object instances that are servlet related (HttpServletRequest, HttpServletResponse, PrincipalProxy, etc.), but it is not the only style used.

Struts2 uses the Spring Framework as the default container, and when doing so the setter method of dependency injection is used. By adding a setter to the action (as shown below), the Struts2 framework will retrieve the correct service from the Spring Framework context and apply it to the action via the setter.

public void setBlogService(BlogService service) {
this.blogService = service;
}
Similar to the interface injection style, we need to include an interceptor - the ActionAutowiringInterceptor interceptor - to the actions interceptor stack. Now, business objects managed by the Spring Framework are injected into Struts2 actions before the action is invoked. Further configuration parameters allow you to set how (by name, by type or automatically) the match between the setter and business object is made.

The Struts Application Code

The starting point is going to be a Struts implementation. For each use case there will be an action class, as well as a class for the action form that will be re-used across all the actions that need it. This may not be the most elegant solution for our application (other solutions include using dynamic forms or using a request dispatch action), but it is a solution that all Struts developers should be familiar with. From the knowledge of converting an uncomplicated implementation, you will have the skills and the knowledge of the Struts2 framework to migrate more advanced implementations.

In Part I we spoke about the differences between a Struts and Struts2 action. Another way of looking at the differences is via UML. Here's what the general form of a Struts action looks like:


The action form will be used across multiple actions, so let's take a look at it first.

public class BlogForm extends ActionForm {

private String id;
private String title;
private String entry;
private String created;

// public setters and getters for all properties
}

As shown in the UML, one restriction is that our form extends the ActionForm class. The second restriction is that the fields are String classes, and hence the getters return String's and the setters accept String's.

The actions that use the action form are the view, create and update actions.

The View Action:

public class ViewBlogAction extends Action {

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

BlogService service = new BlogService();
String id = request.getParameter("id");
request.setAttribute("blog",service.findById(Integer.parseInt(id)));

return (mapping.findForward("success"));
}
}

The Create Action:

public class SaveBlogEntryAction extends Action {

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

BlogService service = new BlogService();
BlogForm blogForm = (BlogForm) form;
Blog blog = new Blog();
BeanUtils.copyProperties( blog, blogForm );

service.create( blog );

return (mapping.findForward("success"));
}
}

The Update Action:

public class UpdateBlogEntryAction extends Action {

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

BlogService service = new BlogService();
BlogForm blogForm = (BlogForm) form;

Blog blog = service.findById( Integer.parseInt(blogForm.getId()));
BeanUtils.copyProperties( blog, blogForm );
service.update( blog );
request.setAttribute("blog",blog);

return (mapping.findForward("success"));
}
}

All three of these actions follow a pattern:

  • The business object instance is created - as mentioned earlier, we are taking the most direct route to using the business object in the actions. This means that a new instance will be created in each action.
  • Data is retrieved from the request - this is in one of two forms. In the view action, the "id" is retrieved from the HttpServletRequest object directly. In the create and update action the ActionForm is used. The ActionForm is very similar to the HttpServletRequest method, the only difference being that the fields are grouped together within an object.
  • The business object is called - it is now time for the business object to be used. If the parameter (in the view action) is a simple object, it can be used after converting to the correct type (from a String to an Integer). If the object is a more complex domain object, the ActionForm needs to be converted using the BeanUtil object.
  • The return data is set - if there is data that needs to be returned so that it can be displayed to the user, it needs to be set as an attribute on the HttpServletRequest object.
  • An ActionForward is returned - the last step in any Struts action is to find and return an ActionForward object.

The last two actions, the remove and the list action, are only slightly different. The remove action, as shown below, doesn't use the BlogForm class. By using a request attribute of "id" (similar to the view action) it can perform the necessary work using the business object. As we will see later in the configuration, it doesn't return any data, as the success result is mapped to execute the list action that will retrieve the necessary information once the record has been removed (thus keeping the concerns of the actions separate).

public class RemoveBlogEntryAction extends Action {

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

BlogService service = new BlogService();
String id = request.getParameter("id");
service.delete(Integer.parseInt(id));

return (mapping.findForward("success"));
}
}

The list action is different because is uses no input from the user. It simply calls the business service with a no-argument method, and returns to the user the list of domain objects that the service returns. Here it is:

public class ListBlogsAction extends Action {

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

BlogService service = new BlogService();
request.setAttribute("bloglist",service.list());

return (mapping.findForward("success"));
}
}

The Conversion to Struts2

In Struts2 there are many ways to implement the above application. These vary from the same use-case-per-class approach (as used in Struts) to creating a class hierarchy, or even a single action class for all use cases. The approach we are going to talk about is what I consider to be the most optimized solution - a single class that implements the CRUD functionality.

Additionally, we will keep the list use case separate. This could be incorporated into the same class, but there would be class attributes that are not used in each use case (the Blog class in the list use case, and the list of Blog classes in all the other use cases) which could become confusing.

For Struts2, we are able to show all the action classes in a single UML model.


Each use case is realized with a method on the action. From the UML diagram above, we see that on the BlogAction we have a method for save, update and remove. The view method is implemented using the execute method, that is inherited from the ActionSupport class. Similarly, on the ListBlogAction, the list use case is implemented using the execute method.

For simplicity, three interfaces have been left off of this diagram for the BlogAction. These are the ServletRequestAware interface, the Prepareable interface and the ModelDriven interface.

The first interface is the ServletRequestAware, which we covered in detail in Part I. Using this interceptor provides access to the HttpServletRequest object in the action, which will allow us to place objects back in to the request to be rendered in the JSP's.

The Preparable interface is next, and works in conjunction with the PrepareInterceptor. When using these two pieces together, a prepare method is provided which is called before the execute method. This allows for setup, configuration or pre-population code in the action.

In our case, the prepare method checks whether this is a new blog or a pre-existing one by checking the value of the blogId field. For a non-zero value, the blog model is retrieved and set on the action.

Next is the ModelDriven interface. In the previous article we saw that one of the most profound differences is that in Struts the actions need to be thread-safe, where in Struts2 there is no such restriction. Each action is instantiated and invoked on each request. Having this restriction removed in Struts2 allows the action class to take advantage of class-level attributes and methods (in particular getters and setters). Combining this with interceptor functionality, allows the attributes in the HttpServletRequest to be set directly on the action.

It goes like this:

  • The list of attribute names in the HTTP request is iterated over
  • A setter for the name of the current attribute is searched for on the current action
  • The value for the attribute name is retrieve from the HttpServletRequest
  • The value is converted to the correct type for the setter in the action
  • The converted value is then applied to the action via the setter

The interceptor that provides this functionality is the ParametersInterceptor interceptor.

TIP: When developing an action, if for some reason the values are not being set correctly, a good first step is to ensure that this interceptor is in the stack applied to the action.

In fact, this is a good strategy to follow at all times. When debugging an issue, if there seems to be something out of place or not working the way that is expected, there is a good chance that it is interceptor related. Check the interceptors that are being applied, and the order in which they are being applied. Interceptors may interfere with each other in ways that you may not expect.

Now that we have the string-based form or request attributes being applied to the actions, the next step is to have them applied to the fields of a domain object or value / transfer object. This is very easy. In fact, the only things that you need to do different as a developer is to implement the ModelDriven interface (which has a single getModel() method) and ensure that the ModelDrivenInterceptor is applied to the action.

Now, instead of finding a setter on the action, the model is first retrieved and checked to see whether it has a setter matching the attribute name. If there is no such setter on the model object, but there is on the action, then the value will be set on the action. We see this flexibility in practice in the BlogAction - as well as the fields for the Blog model object, there is a setId() method on the action. This allows the id of the blog to be set on the action and used in the prepare method to pre-fetch the correct Blog instance, before the values for the fields of the object are set directly on the Blog instance retrieved.

With these two features in place, the implementation of the methods that will be invoked on the action becomes trivial - the specific business service is called, and data to be rendered is placed in the HttpServletRequest.

public class BlogAction extends ActionSupport
implements ModelDriven, Preparable, ServletRequestAware {

private int blogId;
private Blog blog;
private BlogService service = new BlogService();
private HttpServletRequest request;

public void setServletRequest(HttpServletRequest httpServletRequest) {
this.request = httpServletRequest;
}

public void setId(int blogId) {
this.blogId = blogId;
}

public void prepare() throws Exception {
if( blogId==0 ) {
blog = new Blog();
} else {
blog = service.findById(blogId);
}
}

public Object getModel() {
return blog;
}

public String save() {
service.create(blog);
return SUCCESS;
}
public String update() {
service.update(blog);
request.setAttribute("blog",blog);
return SUCCESS;
}

public String remove() {
service.delete(blogId);
return SUCCESS;
}

public String execute() {
request.setAttribute("blog",blog);
return SUCCESS;
}


}

Last is the list action. It also requires access to the HttpServletRequest object to provide data to render and, hence, must implement the ServletRequestAware interface. But, because it takes no input to perform the use case, there is no need for the additional interfaces. Here is the implementation:

public class ListBlogsAction extends ActionSupport implements ServletRequestAware {

private BlogService service = new BlogService();
private HttpServletRequest request;

public void setServletRequest(HttpServletRequest httpServletRequest) {
this.request = httpServletRequest;
}

public String execute() {
request.setAttribute("bloglist",service.list());
return SUCCESS;
}

}

This completes our implementation of the action code. In the final part of the series we will be able to simplify the action even further - when we combine it with a new Struts2 user interface.

Configuring the Actions

Before we can invoke any of the actions from a browser we need to configure them. This is achieved via XML configuration files.

For Struts, we use a file named "struts-config.xml" in the WEB-INF directory, in which we will need to configure two elements - the action form and the action itself. The Struts2 configuration, using a file named "struts.xml' in the classes directory, is a little more complex, as we need to configure interceptors, as well as actions.

The Struts configurations' form-beans node is easy, with attributes for a unique name (provided by the developer) and a type, which is the package and name of the ActionForm class.

<struts-config>

<form-beans>
<form-bean name="blogForm"
type="com.fdar.articles.infoq.conversion.struts.BlogForm"/>
</form-beans>
...

</struts-config>

There are three different ways that we are configuring the actions in the example application.

1. Redirect Configuration

In this configuration no action class is used. Instead, the request is forwarded onto a JSP with no backend processing.

In the Struts configuration, each mapping provides a path element that maps to the URL that will invoke the action, i.e. the path "/struts/add" maps to the URL "/struts/add.do". There is also a forward attribute that provides the URL to forward to - in this case, "/struts/add.jsp".

<struts-config>
...

<action-mappings>

<action path="/struts/add" forward="/struts/add.jsp"/>
...

</action-mappings>
</struts-config>

The Struts2 configuration has more structure to it.

<struts>
<include file="struts-default.xml"/>

<package name="struts2" extends="struts-default" namespace="/struts2">

<action name="add" >
<result>/struts2/add.jsp</result>
</action>
...

</package>
</struts>

The first thing you have probably noticed is that instead of an action-mappings node, there is an include and package node. Struts2 modularizes the configuration by allowing you to sub-divide the configuration into an arbitrary number of files. Each file has exactly the same structure, just different names.

The include node, using the file attribute for the name of the file, inserts the contents of an external file into the current file. The package node groups together actions, and must have a value for the name attribute that is unique.

In the Struts action configuration, the path attribute specified the entire URL. In Struts2, the URL is a concatenation of the namespace attribute of the package, the name attribute of the action node, and the action extension (defaulting to ".action"). The action above would then be invoked by "/struts2/add.action".

The last attribute of the package node is the extends attribute. As well as providing namespace separation, packages also provide structure. By extending another package you gain access to its configuration - actions, results, interceptors, exceptions, etc. The "struts2" package above extends the "struts-default" package (defined in the included file "struts-default.xml") - this is the master include file that should be the first line of all configurations. It will save you typing by providing all the default configurations for result types, interceptors, and the more common interceptor stacks that can be used.

Last is the result node, and is just a value for the URL to forward to. What we have left out are the name and type attributes. Unless you are changing these from the default values, you can leave them out, making the configuration simpler. The default values render a JSP for a "success" result being returned from the action.

2. Action Configuration

An action class is invoked to provide backend processing, the result from processing is defined in the configuration and the user redirected to the corresponding view.

This is the next step from a configuration that is a redirect configuration. There are two additional attributes on the action node. The type attribute provides the package and name of the action class, and the scope attribute ensures that any form beans (if used) are placed in the request scope.

Instead of a forward attribute, the action configuration uses a forward node. There should be one node for each and every result that the action can return.

<struts-config>
...

<action-mappings>

<action path="/struts/list" scope="request"
type="com.fdar.articles.infoq.conversion.struts.ListBlogsAction" >
<forward name="success" path="/struts/list.jsp"/>
</action>
...

</action-mappings>
</struts-config>

In the case of an action configuration, the XML for the Struts2 configuration is similar to before. The only difference being that the package and name of the action to be invoked is provided via the class attribute on the action node.

<struts>
...

<package name="struts2" extends="struts-default" namespace="/struts2">

<default-interceptor-ref name="defaultStack"/>

<action name="list"
class="com.fdar.articles.infoq.conversion.struts2.ListBlogsAction">
<result>/struts2/list.jsp</result>
<interceptor-ref name="basicStack"/>
</action>
...

</package>
</struts>

If a method other than the execute method is to be invoked (which will be the case for most of the configurations referencing the BlogAction class), a method attribute provides the name of the method. In the example below, the update method would be invoked.

    <action name="update" method="update"
class="com.fdar.articles.infoq.conversion.struts2.BlogAction" >
...
</action>

The difference comes from the default-interceptor-ref and the interceptor-ref nodes. In Part I, we saw how the request passes through a series of interceptors before the action is invoked, these nodes configure the interceptors. The default-interceptor-ref node provides the name of the interceptor stack to use as the default for the package. When the interceptor-ref node is provided, it overrides the default interceptor (the name attribute on the interceptor-ref node can reference either a single interceptor or a stack of interceptors that have been previously configured). Additionally, multiple interceptor-ref nodes can be provided, with processing occurring in the order that they are listed.

3. Post-Redirect Configuration

The final configuration we are using is for submitting forms when there is an additional requirement that refreshing the resulting page should not re-submit the form. This is known as the "post-redirect pattern" or, more recently, "flash scope."

As this is a form, we need to specify the ActionForm that Struts will be using. This is achieved by providing the name of the form (configured above) in the name attribute of the action node. The only other change that is needed is setting the redirect attribute to true in the forward node.

<struts-config>
...

<action-mappings>
<action path="/struts/save"
type="com.fdar.articles.infoq.conversion.struts.SaveBlogEntryAction"
name="blogForm" scope="request">
<forward name="success" redirect="true" path="/struts/list.do"/>
</action>
...

</action-mappings>
</struts-config>

Rather than augmenting the existing configuration, Struts2 provides the post-redirect functionality through a new type of result. So far we have used the default "dispatch" result type, but there are many different result types available. The result used here is the "redirect" type.

<struts>
...

<package name="struts2" extends="struts-default" namespace="/struts2">

<action name="save" method="save"
class="com.fdar.articles.infoq.conversion.struts2.BlogAction" >
<result type="redirect">list.action</result>
<interceptor-ref name="defaultStack"/>
</action>
...

</package>
</struts>

Once again, we are using the default result of "success".

Wrap-Up

We've been able to cover a lot in this article, but there are some things that we didn't have time for. For a better understanding of the framework and additional implementation options, here is a short list of things to take a look at:

  • Configuring interceptors and interceptor stacks - take a look at the "struts-default.xml" file in the struts2-core JAR file for examples. Creating your own interceptors for cross-cutting application features can be a great time saver, and the examples in "struts-default.xml" will show you how to configure your own application-based interceptor stacks that includes the new interceptor.
  • Wildcard patterns in configuration files - as well as typing everything out, there is an option of using wildcard patterns in Struts2. This is a port of the Struts implementation to Struts2.
  • Utilize UI property maps using the ParameterAware interface - instead of having a model / transfer / value object or specific attributes on a class, you can configure Struts2 to place all the request or form attributes into a map in the action. This simulates the dynamic form feature of Struts.

Another question you might be asking yourself is "does this really work with the same UI?" - and the answer is yes. I have included the full source code for the examples in this article, and you will see from these that the only change necessary is to modify the extension of the URL being called (from ".do" to ".action"). Additionally, the Struts taglibs (for forms) could have been easily used instead of JSTL. I will leave this as an exercise for interested readers.

In the next and final article in the series we look at the user interface. We will talk about the architecture; look into themes and tags; talk about how validation fits into the picture; and discuss ways to re-use code using UI components. After which, we will have a fully transformed application.

About the author

Ian Roughley is a speaker, writer and independent consultant based out of Boston, MA. For over 10 years he has been providing architecture, development, process improvement and mentoring services to clients ranging in size from fortune 10 companies to start-ups. Focused on a pragmatic and results-based approach, he is a proponent for open source, as well as process and quality improvements through agile development techniques.

Rate this Article

Adoption
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

  • Version Numbers

    by Corby Page,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Not sure that I understand the numbering scheme.

    What will be the version number for the first Production quality release of Struts 2?

  • Re: Version Numbers

    by Don Brown,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Struts uses the Tomcat naming system, so numbered releases are created first, then after a week or so, they are voted on to determine their quality. In this case, 2.0.1 has been created, but the vote for its quality hasn't happened yet. My vote will probably be for beta due to some broken showcase examples, although we are really close to GA.

  • A bit confused...

    by Alex Wibowo,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Hi,

    Firstly, thanks for writing the article. I enjoy reading through it.


    I have a question regarding what you wrote in this 2nd tutorial, in particular about the "ModelDriven" interface. From the sample code, it seems that this interface specifies the "getModel()" method. When does this method gets called (which presumably by the framework itself - i think)?

    "Now, instead of finding a setter on the action, the model is first retrieved and checked to see whether it has a setter matching the attribute name. If there is no such setter on the model object, but there is on the action, then the value will be set on the action."
    --> Isnt the model null initially? The framework wants to call setId().. and it
    tries to do so on the model first. But the model gets initialized on the prepare() method. But looking at your prepare() method, it needs the "blogId", which is initialized on the setId() method. So by that time, "blogId" is null, and hence.. everything does not work. I'm sure I'm missing an important point here. It would be great if you can enlighten me :)

  • XML

    by Dorel Vaida,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Still didn't find a way to get rid of all that XML ? AFAIK that was one of the main drawbacks people have complained about over the years (not considering those heavy OOP-ists who thought forms are BS :-) )... Eventually some code-by-convention + annotations (I know it's arguable but 99% of the devs will use struts as a WEB framework so it isn't such a shame to place configuration like form, success view etc. in action classes) overriden from XML if somebody really wants it ?

  • Re: XML

    by Ian Roughley,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Yes - there is definately a lot of XML in the configurations presented. The good news is that there is a large effort being put forward to move configurations to a common convention (so no specification is required) as well as providing annotation.

    As this article was about converting application, it made sense to provide the XML configurations - as this is what most people using Struts would be farmiliar with.

  • Re: A bit confused...

    by Ian Roughley,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Sure - no problem. The trick is in the interceptor stack.

    The interceptor stack for the action in question is the "paramsPrepareParamsStack". The interceptors for this stack go like this... (1) params - to apply parameters to the action (so the id is set on the action), (2) prepare - to call the prepare() method using the id that has been previously set, and then (3) params again to set the values on the model.

    Please note that there is a little redundancy here, as the action and possibly the model will not have the setters for the parameters that are trying to be set against them.

    PS - Sorry for the delay in answering, I have been on vacation.

  • Re: A bit confused...

    by Subhash Murthy,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Please let me know how to configure/write action class such that the form input values are populated directly into variables in the action class. (as in struts 1 form bean) without using Maps (which can be done using ParameterAware Interceptor).
    I tried by implementing ModelDriven intercpetor as in struts2 tutorial but in vain to get them into the action class.
    Please help me.
    -Subhash

  • source code missing classes.

    by Man Moon,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    com.fdar.articles.infoq.conversion.Blog
    com.fdar.articles.infoq.conversion.BlogService

  • Re: source code missing classes.

    by Dhiraj P,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    hi....i tried to download the source codes from the links: www.infoq.com/resource/articles/migrating-strut... and www.infoq.com/resource/articles/migrating-strut... but couldnt download....are the links still alive?
    Thanking you,
    Dhiraj

  • Re: A bit confused...

    by Ian Roughley,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    You need to name the form parameters the same as the names in the model. You might want to try the Struts mailing list for this type of question.

  • Dumb Question

    by David PIchard,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    So, after reading several papers on converting a struts 1 app to struts 2 can someone please restate the obvious for me: In converting my app I am going to have to rewrite the tag info in all my JSPs as well as modify all my classes to some degree? I had read an article or two leading me to believe struts 1 and 2 could peacefully co-exist and all new features of the app could be written to use struts2... leaving existing functionality in place... Any input would be greatly appreciated...

    - David

  • Forward to the action in Struts 2

    by jan Vondr,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Good article Thanks
    But in article is this solution for forwarding to list.action

    <result type="redirect">list.action</result>

    I think in struts 2 is better to write

    <result type="redirect-action">list</result>

  • Re: source code missing classes.

    by Rong Wang,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Did you find the two missing classes? Thanks.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT