09 Apr 2013 Integrating SpringMVC with OpenCms
OpenCms websites tend to place a lot of responsibility for the controller and view functionality of a page on the JSP, a JEE anti-pattern known as a Monolithic/Compound JSP. We recently integrated SpringMVC into an OpenCMS website at one of our clients to gain the advantages of a Model-View-Controller framework without an extensive rewrite of the codebase.
Problem Solving
Many pages in the current website had long suffered from being written as Monolithic/Compound JSPs. Problems include untested functionality, simple changes to the view requiring a back end developer to implement, not to mention barely readable page source, making JSP-level debugging a tedious task.
SpringMVC provides a solution to this by separating the view logic from the controller logic, the division simplifying the responsibility of each component. By integrating SpringMVC into the OpenCms website, our goal is to make our page controller code more robust using a technology familiar to web developers. We had several requirements/constraints when planning this integration:
- Existing pages should still work without the need for modification.
- We wanted to retain the concept of page requests mapping to OpenCms resources. Moving, renaming, or previewing OpenCms resources by content editors shouldn’t break the Controller-View relationship.
- The solution should be otherwise non-disruptive to the current website.
Our proposed solution involves SpringMVC running alongside OpenCms:
In order to achieve this, there needed to be a way of linking to a Spring controller when an OpenCms resource is called, as well as a linking an OpenCms resource to the view name returned by the controller. We decided to leverage the flexibility of OpenCms resource properties to specify whether a particular resource should call a Spring controller, or whether a particular resource should be returned as a view (or both). We introduced two new properties:
- SpringInitialisationURL – a string that specifies the path to a request handler in a Spring controller.
- SpringViewName – a string that specifies a view name for the OpenCms resource.
We would then need a way for OpenCms to communicate to the Spring controller, and vice versa; the glue between the two technologies. This is implemented using a custom Rewrite Filter and View Resolver.
The Rewrite Filter
The Rewrite Filter will be used to forward from a Spring-enabled OpenCms resource to the corresponding Spring controller using the specified initialisation URL. Using the OpenCms API (in particular, the CmsObject class), we are able to inspect the properties of a resource when it is requested, allowing us to test for a SpringInitialisationURL property. To provide this functionality, we have introduced a helper class called MvcCmsUtil.
public class MvcCmsUtil { ... private static final String SITE_ROOT="/sites/default"; public static final String SPRING_INITIALISATION_URL = "springInitialisationUrl"; ... public CmsObject getOnlineObjectSiteRootAsGuest() throws CmsException { CmsObject cmsObj = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserGuest()); cmsObj.getRequestContext().setSiteRoot(SITE_ROOT); return cmsObj; } public String getOnlineProperty(String sResourcePath, String sProp) throws CmsException { CmsObject cmsObj = getOnlineObjectSiteRootAsGuest(); cmsObj.getRequestContext().setSiteRoot(SITE_ROOT); CmsProperty prop = cmsObj.readPropertyObject(sResourcePath, sProp, true); String sValue = prop.getValue(); return sValue; } ... }
The getOnlineProperty method will attempt to retrieve a resource at a specified path, and return the value of a given property – in this case, the Spring initialisation URL to forward the request on to:
public class RewriteFilter implements Filter { @Autowired private MvcCmsUtil mvcCmsUtil; public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ... String springInitialisationUrl = mvcCmsUtil.getProperty(cmsResourcePath, MvcCmsUtil.SPRING_INITIALISATION_URL); if (null != springInitialisationUrl) { RequestDispatcher dispatcher = request.getRequestDispatcher(springInitialisationUrl); dispatcher.forward(request, response); return; } ... } }
The Tomcat configuration needs to be able to differentiate between requests to the OpenCms servlet and the Spring dispatcher servlet. In the web.xml configuration, this is done by mapping all requests beginning with “/spring” to the dispatcher servlet. Request handlers in the Spring controller should reflect this naming convention.
<servlet> <servlet-name>OpenCmsServlet</servlet-name> <description> The main servlet that handles all requests to the OpenCms VFS. </description> <servlet-class>org.opencms.main.OpenCmsServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:dispatcher-servlet.xml</param-value> </init-param> <load-on-startup>3</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/spring/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>OpenCmsServlet</servlet-name> <url-pattern>/opencms/*</url-pattern> </servlet-mapping>
Lastly, the corresponding request handler in the Spring controller itself needs to be setup to accept these requests.
@RequestMapping(value="/spring/controller/myController", method = RequestMethod.GET) public String myController(HttpServletRequest request, HttpServletResponse response) { // Controller logic goes here return "myView"; }
This sample controller will be called when “/spring/controller/myController” is set as the value of the SpringInitialisationURL on the OpenCms resource. The next step is resolving the view name “myView” in this example back to an OpenCms resource, using a View Resolver.
The View Resolver
The CmsObject class has a method to retrieve a resource by a given property value. We can use this to find a resource with a matching Spring view name. To do this, we have expanded the MvcCmsUtil helper class to include a new method:
public class MvcCmsUtil { ... public static final String SPRING_VIEW_NAME = "springViewName"; public static final String CMS_CONTENT_PATH = "/content/"; ... public String findCmsRessourceByViewName(String viewNameWithParameters) throws CmsException { String[] viewNameWithParametersArray = viewNameWithParameters.split("\\?"); String viewName = viewNameWithParametersArray[0]; CmsObject cmsObject = getOnlineObjectSiteRootAsGuest(); List cmsResources = (List) cmsObject.readResourcesWithProperty(CMS_CONTENT_PATH, SPRING_VIEW_NAME, viewName); for (CmsResource cmsResource : cmsResources) { if (viewName.equals(cmsObject.readPropertyObject(cmsResource, SPRING_VIEW_NAME, false).getValue())) { if (viewNameWithParametersArray.length > 1) { return cmsObject.getSitePath(cmsResource) + "?" + viewNameWithParameters.split("\\?")[1]; } else { return cmsObject.getSitePath(cmsResource); } } } return null; } ... }
The above method will retrieve a list of resources that have the “springViewName” property filled, and will return a resource that matched the view name given. Some extra work is done to handle returning view names with URI parameters. A View Resolver – registered in the dispatcher servlet – needs to then call our method in order to return the correct OpenCms resource.
public class OpenCmsViewResolver extends InternalResourceViewResolver { private static final String OPEN_CMS_SERVLET_PATH = "/opencms"; @Autowired private MvcCmsUtil mvcCmsUtil; @Override public View resolveViewName(String name, Locale locale) throws Exception { String cmsResourcePath = OPEN_CMS_SERVLET_PATH + mvcCmsUtil.findCmsRessourceByViewName(name); return super.resolveViewName(cmsResourcePath, locale); } }
The Benefits (after all that)
We are now able to create an entry point to a Spring controller anywhere within the logical structure of the OpenCms website by configuring a single property – the SpringInitialisationURL of a resource. Since this functionality depends on whether this property is set, it integrates seamlessly into the existing OpenCms website with no impact to existing pages. Being able to configure the entry point and Spring view independently of the controller itself offers greater flexibility and ultimately requires less changes to the code base in the future.
Furthermore, by offloading more responsibility to a Java class for the controller logic of a page, we are ensuring that future pages are more easily testable, and reducing the amount of clutter and code irrelevant to the view in the JSP itself.
Issues & Considerations
This implementation only retrieves the online properties of the OpenCms resource. Therefore, if a developer is working in offline mode, they must first publish the resource before the Spring controller can be called. This is because MvcCmsUtil currently accesses a given OpenCms resource as a guest user only. This could be improved to retrieve an OpenCms object instance from the current session. One reason this might be useful is that several administrative pages require the user to be logged in to OpenCms to access certain functions. Using the CmsUser object, retrieving the logged in user from the session would allow us to discern whether someone accessing a Spring controller has the appropriate privileges.
A second issue lies with the robustness of this solution. For example, in its current state there is no restriction that that view name of a resource must be unique, and multiple resources with the same view name will result in only the first resource in the list returned from the readResourcesWithProperty() method call being returned by the controller. Addressing these issues would require further consideration and development.
Conclusions
What we have achieved is allowing an existing website with a JSP-Controller oriented approach to leverage the benefits of a Model-View-Controller framework without the need to rewrite the codebase or drastically alter its current implementation. In doing so, we have made the process relatively straightforward for existing developers to take advantage of, and have increased our effective test code coverage by taking the responsibility of the controller logic away from the JSP and into a Java controller that is easier to unit test.
chandrajeet
Posted at 05:11h, 10 SeptemberHi, Can I have this sample code to test out. I am trying a similar integration with opencms.
thanks,
Pingback:OpenCMS integration with Spring MVC | My Blog
Posted at 22:49h, 04 February[…] http://blog.shinetech.com/2013/04/09/integrating-springmvc-with-opencms/ […]
surya
Posted at 14:43h, 10 AugustWhere do I make these changes ?Either in opencms.war or spring webapplication. Can I have this sample code?