Creating a Customized Solution With JBoss Portal
Join the DZone community and get the full member experience.
Join For FreeOut of the box, the JBoss Portal project delivers a simple portal, usually users will strip it down completely and start fresh or build on top of it. In this article we will see how to progressively transform the out of the box portal to a customized solution.
From:
To:
Changing the theme
The first thing we will do is to change the visual aspect of the portal. JBoss Portal separates the layout (how a page is shaped) and it's artistic aspect (usually a CSS file). In this example we will create both a new layout and a new theme (or skin). For this article we've found an existing theme designed for a blog under a public domain license which can be found here: http://www.opendesigns.org/design/?template=1090
The downloaded file was composed of a static HTML page, a CSS file and few images and then was easy to adapt, we just needed to remove the static parts to add the dynamic ones. The HTML page became our layout and the CSS file our skin. Also since we wanted to have three different layouts at the end we've splitted the documents with header and footer parts.
Here here a graphical description of the split, surrounded in red we can see the header (the footer doesn't fit on the page), on the top in the green rectangle we see the position where we want to include links, on the left a two column regions where we want to put windows and on the right another region for other windows. On top of the right region we've added a place for Login/logout links.
Our three columns layout now looks like:
<%@ taglib uri="/WEB-INF/theme/portal-layout.tld" prefix="p" %>
<%@include file="/layouts/common/header.jsp"%>
<p:region regionName='FirstColumn' regionID='linkbar'/>
<p:region regionName='SecondColumn' regionID='left'/>
<div id="right">
<div class="content">
<!-- Utility controls -->
<p:region regionName='dashboardnav' regionID='dashboardnav'/>
<p:region regionName='MainColumn'/>
</div>
</div>
<%@include file="/layouts/common/footer.jsp"%>
As we can see, we used a JBoss Portal specific tag library to define places where the portal framework should include a region that we are naming FirstColumn, a region called dashboardnav (that will include Login/Logout links) and a region called SecondColumn. The linked CSS (which is standard CSS) will display things as we want. This layout doesn't specify how we want windows to be displayed though.
On the next image we can see how a page is decomposed, we've seen how to declare where in the markup we want to include the regions. We still have to define what markup we want to define the region, markup for each window, markup for the window decoration which usually consists of the window's title and links for portlet modes and for the markup around the content given by the portlet itself.
In our case we don't want any window decoration except the window title. We could have different kind of window decorations but what is important to us is to be able to reuse those across the portal..
We will define a class for all the four elements region, window, decoration and portlet implementing the Java interfaces org.jboss.portal.theme.render.renderer.RegionRenderer, WindowRenderer, DecorationRenderer and PortletRenderer. In our example we can find the classes in org.jboss.portal.myportal.theme.*
Let's just have a look at our WindowRenderer that simply adds a “div” tag around the window then delegates decoration rendering and portlet rendering.
package org.jboss.portal.myPortal.theme;
import [...]
import org.jboss.portal.theme.render.AbstractObjectRenderer;
import org.jboss.portal.theme.render.renderer.WindowRenderer;
import org.jboss.portal.theme.render.renderer.WindowRendererContext;
public class DivWindowRenderer extends AbstractObjectRenderer
implements WindowRenderer
{
public void render(RendererContext rendererContext, WindowRendererContext wrc) throws RenderException
{
Writer writer = rendererContext.getWriter();
try {
writer.write("<div>");
rendererContext.render(wrc.getDecoration());
rendererContext.render(wrc.getPortlet());
writer.write("</div>");
} catch (IOException e) {
e.printStackTrace();
}
}
}
The decoration renderer simply renders the title of the window:
package org.jboss.portal.myPortal.theme;
import [...]
import org.jboss.portal.theme.render.AbstractObjectRenderer;
import org.jboss.portal.theme.render.RenderException;
import org.jboss.portal.theme.render.RendererContext;
import org.jboss.portal.theme.render.renderer.DecorationRenderer;
import org.jboss.portal.theme.render.renderer.DecorationRendererContext;
public class DivDecorationRenderer extends AbstractObjectRenderer
implements DecorationRenderer
{
public void render(RendererContext rendererContext, DecorationRendererContext drc) throws RenderException
{
PrintWriter out = rendererContext.getWriter();
out.println("<span class=\"headline_three\">" + drc.getTitle() + "</span><br />");
}
}
We now have defined all markup and CSS required to display the content. We bundled those classes in a JAR and add the static resources within a WAR. The only thing left for the theming part is to declare our new layout and skin. To do that, we need to add few descriptors. First we need to declare a renderset which is a set of renderers. With those rendersets we can combine several existing renderers to pick from as we will build out portal.
In our myPortal.war/WEB-INF/layout/portal-renderSet.xml we'll give a name to our new renderset and defines all the renderers to use:
<portal-renderSet>
<renderSet name="myPortalRenderer">
<set content-type="text/html">
<ajax-enabled>false</ajax-enabled>
<region-renderer>org.jboss.portal.myPortal.theme.DivRegionRenderer</region-renderer>
<window-renderer>org.jboss.portal.myPortal.theme.DivWindowRenderer</window-renderer>
<portlet-renderer>org.jboss.portal.myPortal.theme.DivPortletRenderer</portlet-renderer>
<decoration-renderer>org.jboss.portal.myPortal.theme.DivDecorationRenderer</decoration-renderer>
</set>
</renderSet>
</portal-renderSet>
Now we need to define our layouts in myPortal.war/WEB-INF/portal-layouts.xml:
<layouts>
<layout>
<name>MyLayout</name>
<uri>/layouts/myLayout.jsp</uri>
<uri state="maximized">/layouts/myMaximizedLayout.jsp</uri>
<regions>
<region name="FirstColumn"/>
<region name="SecondColumn"/>
<region name="MainColumn"/>
</regions>
</layout>
<layout>
<name>TwoColumnsLayout</name>
<uri>/layouts/twoColumnsLayout.jsp</uri>
<uri state="maximized">/layouts/myMaximizedLayout.jsp</uri>
<regions>
<region name="FirstColumn"/>
<region name="SecondColumn"/>
</regions>
</layout>
</layouts>
Last but not least let's define our skin in myPortal.war/WEB-INF/portal-themes.xml and give it a name.
<themes>
<theme>
<name>mySkin</name>
<link href="/skins/mySkin.css" title="" rel="stylesheet" type="text/css" media="screen" />
</theme>
</themes>
With this war deployed in the out of the box portal, we would already be able to change the default theme by our own by using the admin portlet for example.
Applied to the front page, this would look like the next screenshot. As the region names have changed from the original page, existing windows aren't shown but links to the other pages are here with the Login/Logout links.
Same page, after adding some portlets/CMS content that fits well with the theme.
But we are not quite there yet, localization has not been handled neither how to add content, get proper declarative security and caching.
Adding content
For the purpose of this article we have built a simple RSS portlet that is able to either show a list of last n entry titles of a blog or the full content of an article (if provided in the feed). This can be found in the rssPortlet project. We won't go into the details of the portlet itself, it is a standard JSR-286 portlet that supports two parameters, 'rssFeed' which contains the URL of a RSS feed and 'limit' which is the maximum number of entries to show.
What matters here is how can we add windows using that portlet, and for this article we don't want to use the admin portlet but we want to be able to ship the portal to someone so that on first start up it will fill its database with correct values. We will then use XML to define our portal. Inside our portlet web archive, we include a file called WEB-INF/portlet-instances.xml that will be used to create customized instances of the portlet.
The file is pretty self-explanatory, we create three instances for different feeds with different values for the RSS Feed to display. Note that the last instance has a security constraint, it is a declarative way to show that instance only to users with the admin role.
<!DOCTYPE deployments PUBLIC
"-//JBoss Portal//DTD Portlet Instances 2.6//EN"
"http://www.jboss.org/portal/dtd/portlet-instances_2_6.dtd">
<deployments>
<deployment>
<instance>
<instance-id>JBossPortalRSSInstance</instance-id>
<portlet-ref>MyRSSPortlet</portlet-ref>
<preferences>
<preference>
<name>rssFeed</name>
<value>
http://feeds.feedburner.com/jbossportal
</value>
</preference>
<preference>
<name>limit</name>
<value>10</value>
</preference>
</preferences>
</instance>
</deployment>
<deployment>
<instance>
<instance-id>JBossRSSInstance</instance-id>
<portlet-ref>MyRSSPortlet</portlet-ref>
<preferences>
<preference>
<name>rssFeed</name>
<value>http://labs.jboss.org/feeds/all/atom</value>
</preference>
<preference>
<name>limit</name>
<value>10</value>
</preference>
</preferences>
</instance>
</deployment>
<deployment>
<instance>
<instance-id>AdminOnlyRSSInstance</instance-id>
<portlet-ref>MyRSSPortlet</portlet-ref>
<security-constraint>
<policy-permission>
<action-name>view</action-name>
<role-name>Admin</role-name>
</policy-permission>
</security-constraint>
<preferences>
<preference>
<name>rssFeed</name>
<value><![CDATA[http://pipes.yahoo.com/pipes/pipe.run?_id=fb3504450e04190b33a8ef9628599c76&_render=rss&forumurl=215]]></value>
</preference>
<preference>
<name>limit</name>
<value>10</value>
</preference>
</preferences>
</instance>
</deployment>
</deployments>
With those instances of portlet, we will be able to add them in windows that will compose our pages. We've decided to store the composition of our portal along with the new theme in myPortal.war. The file of interest here is located at myPortal.war/WEB-INF/default-object.xml. Since it's a bit too long to put entirely on this article, we will look at some parts.
The first deployment is the context itself, the root of all portals, it's where all default values can be defined as any child object will inherit from the context, we define here that we want to use our new layout, our new skin, our renderset and define the name of the portal to use as default (we'll name it 'default' here).
<deployments>
<deployment>
<context>
<context-name />
<properties>
<!--
| Set the layout for the default portal, see also portal-layouts.xml.
-->
<property>
<name>layout.id</name>
<value>MyLayout</value>
</property>
<!--
| Set the theme for the default portal, see also portal-themes.xml.
-->
<property>
<name>theme.id</name>
<value>mySkin</value>
</property>
<!--
| Set the default render set name (used by the render tag in layouts), see also portal-renderSet.xml
-->
<property>
<name>theme.renderSetId</name>
<value>myPortalRenderer</value>
</property>
<!--
| The default portal name, if the property is not explicited then the default portal name is "default"
-->
<property>
<name>portal.defaultObjectName</name>
<value>default</value>
</property>
[...]
</context>
</deployment>
The next deployment is our default portal, we define our pages and windows. See how we define a window with a reference to the portlet id ( Or CMS reference, Gadget reference...)., the name of the region where to put the window, and a number to define the ordering within the region.
We can also notice that the second page overrides the value for the layout to use to use a two column layout that we've defined instead of the default three column layout we've seen earlier.
All pages of this snippet are granted the right to be viewed by anybody.
<deployment>
<parent-ref />
<if-exists>overwrite</if-exists>
<portal>
<portal-name>default</portal-name>
<supported-modes>
<mode>view</mode>
<mode>edit</mode>
<mode>help</mode>
</supported-modes>
<supported-window-states>
<window-state>normal</window-state>
<window-state>minimized</window-state>
<window-state>maximized</window-state>
</supported-window-states>
<security-constraint>
<policy-permission>
<action-name>view</action-name>
<unchecked />
</policy-permission>
</security-constraint>
[...]
<page>
<page-name>default</page-name>
<display-name xml:lang="en">Home</display-name>
<display-name xml:lang="fr">Page de Garde</display-name>
<security-constraint>
<policy-permission>
<action-name>view</action-name>
<unchecked />
</policy-permission>
</security-constraint>
<properties>
<property>
<name>order</name>
<value>0</value>
</property>
</properties>
<window>
<window-name>JBossPortalRSSWindow</window-name>
<instance-ref>JBossPortalRSSInstance</instance-ref>
<region>FirstColumn</region>
<height>0</height>
</window>
<window>
<window-name>JBossRSSWindow</window-name>
<instance-ref>JBossRSSInstance</instance-ref>
<region>SecondColumn</region>
<height>0</height>
</window>
<window>
<window-name>CMSWindow</window-name>
<content>
<content-type>cms</content-type>
<content-uri>/default/index.html</content-uri>
</content>
<region>MainColumn</region>
<height>1</height>
</window>
</page>
<page>
<page-name>otherpage</page-name>
<display-name xml:lang="en">Other Page</display-name>
<display-name xml:lang="fr">Autre Page</display-name>
<security-constraint>
<policy-permission>
<action-name>view</action-name>
<unchecked />
</policy-permission>
</security-constraint>
<properties>
<property>
<name>order</name>
<value>1</value>
</property>
<property>
<name>layout.id</name>
<value>TwoColumnsLayout</value>
</property>
<property>
<name>theme.id</name>
<value>mySkin</value>
</property>
</properties>
<window>
<window-name>SudokuWindow</window-name>
<content>
<content-type>widget/netvibes</content-type>
<content-uri>
http://sudokushark.com/netvibes_uwa.php
</content-uri>
</content>
<region>FirstColumn</region>
<height>0</height>
</window>
<window>
<window-name>JBossForumRSSWindow</window-name>
<instance-ref>AdminOnlyRSSInstance</instance-ref>
<region>SecondColumn</region>
<height>0</height>
</window>
</page>
[...]
</portal>
</deployment>
Before we deploy this in JBoss Portal, we should remove the default out of the box configuration, this can be done by deleting jboss-portal.sar/conf/data/default-object.xml. Also we should empty our database by deleting jboss-4.2.3/server/default/data to make sure our content will be in sync with what we have described here.
Also we want to customize some CMS content, again we could have create content through the CMS Admin portlet but here we simply want to add content on startup.
Declarative security and caching
We have already seen how we can restrict a portlet instance for a particular role, below is a partial definition of a page that is restricted to the role 'admin'. Any logged-in standard user will not be able to access the page.
<page>
<page-name>admin</page-name>
<display-name xml:lang="en">Admin Only</display-name>
<display-name xml:lang="fr">Pour Admins</display-name>
<security-constraint>
<policy-permission>
<action-name>view</action-name>
<role-name>Admin</role-name>
</policy-permission>
</security-constraint>
[...]
</page>
Another given feature is the possibility to define page fragment caching. There are cases where calling the render phase of a portlet doesn't make sense as the content would not change over time or not often. Our RSS portlet is a typical example and here we want to cache the content of the portlet for 10min to avoid unnecessary expensive calls to the feed.
Using the standard portlet.xml descriptor we add an expiration cache in seconds:
<portlet>
<description>My RSS Portlet</description>
<portlet-name>MyRSSPortlet</portlet-name>
<display-name>My RSS Portlet</display-name>
<portlet-class>org.jboss.portal.rssPortlet.RSSPortlet</portlet-class>
<expiration-cache>600</expiration-cache>
[...]
At this stage, we have built a portal with organized content, security and caching.
Changing internals
The out of the box portal determines the user locale from the user's profile, if the user profile didn't set a preferred language, the one set by its web browser is used.
In our sample portal we want to let the user click on flags. This is our requirement.
To implement this requirement, we decide to store the chosen locale in the session of the portal itself. To do so we need to edit jboss-portal.sar/portal-server.war/WEB-INF/web.xml and add a Servlet definition and servlet mapping:
<servlet>
<servlet-name>localeServlet</servlet-name>
<servlet-class>org.jboss.portal.myPortal.servlet.LocaleServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>localeServlet</servlet-name>
<url-pattern>/locale</url-pattern>
</servlet-mapping>
The servlet simply takes the parameter obtained from GET parameter, store it in the session as "org.jboss.portal.myPortal.locale” and redirect the user to where he came from:
package org.jboss.portal.extension.servlet;
import [...]
public class LocaleServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
String country = request.getParameter("country");
String language = request.getParameter("language");
Locale locale = new Locale(language, country);
request.getSession(true).setAttribute("org.jboss.portal.myPortal.locale", locale);
response.sendRedirect(request.getParameter("from"));
}
}
In the layout, we've added two flag images with links to the Servlet:
<a href="<%= request.getAttribute("org.jboss.portal.PORTAL_CONTEXT_PATH") %>/locale?country=en&language=en&from=<%= request.getRequestURI() %>"><img src="<%= request.getContextPath() %>/images/flags/en.gif" alt="English"/></a>
<a href="<%= request.getAttribute("org.jboss.portal.PORTAL_CONTEXT_PATH") %>/locale?country=ch&language=fr&from=<%= request.getRequestURI() %>"><img src="<%= request.getContextPath() %>/images/flags/fr.gif" alt="French"/></a>
Now to replace the default behavior to take the locale from the session instead of the web browser or user profile we need to replace the default interceptor defined in jboss-portal.sar/META-INF/jboss-service.xml
Instead of this MBean:
<mbean code="org.jboss.portal.core.aspects.server.LocaleInterceptor"
name="portal:service=Interceptor,type=Server,name=Locale" xmbean-dd=""
xmbean-code="org.jboss.portal.jems.as.system.JBossServiceModelMBean">
<xmbean/>
</mbean>
We create our own org.jboss.portal.extension.aspect.LocaleInterceptor:
package org.jboss.portal.extension.aspect;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import org.jboss.portal.common.invocation.InvocationException;
import org.jboss.portal.server.ServerInterceptor;
import org.jboss.portal.server.ServerInvocation;
import org.jboss.portal.server.ServerRequest;
public class LocaleInterceptor extends ServerInterceptor {
protected void invoke(ServerInvocation invocation) throws Exception,
InvocationException {
HttpServletRequest request = invocation.getServerContext().getClientRequest();
Locale locale = (Locale)request.getSession().getAttribute("org.jboss.portal.myPortal.locale");
if (locale == null)
{
locale = Locale.ENGLISH;
}
ServerRequest req = invocation.getRequest();
// Set the locale for the request
Locale[] tmp = new Locale[]{locale};
req.setLocales(tmp);
// Invoke next interceptors
invocation.invokeNext();
}
}
We replace the MBean core value to our new class and don't forget to include the jar including this class in jboss-portal.sar/lib.
Putting everything together
All sources for this demo are available on SVN:
http://anonsvn.jboss.org/repos/portal/other/dzone_article
The readme.txt files explains how to setup and start the customized portal. If correctly setup you should be able to enjoy a visually customized portal using declarative security and caching, with a customized way to define the preferred language and a new web application creating content for a fragment of a page.
Opinions expressed by DZone contributors are their own.
Comments