Building a Site Map in WebLogic Portal

Introduction

Over the course of the years, WebLogic has improved significantly in both features and performance. However sometimes with those improvements we need to learn new ways to do things in order to best leverage those changes. Prior to the introduction of tree optimization in 8.1 SP4, creating a site map was a trivial task as you could simply walk the tree of presentation contexts starting with DesktopPresentationContext.

It was trivial because prior to 8.1 SP4, Portal would build the complete control tree where each control in the tree would represent a portal artifact, for example a book, page or portlet. This made walking the tree for a site map quite easy since all artifacts are present. There was a dark side to this however as customers with large portals found that the performance of building the control tree was abysmal and often resorted to techniques like splitting a portal across multiple desktops to work around this issue.

In 8.1 SP4 tree optimization was introduced. This feature, which is on by default, tells the Portal to build a partial control tree rather then a complete one. This means that only the controls that are visible will be in the tree. This greatly improves performance, however it complicates creating a site map since we can no longer walk the available contexts.

Building the Site Map

When developers new to Portal encounter the issue with tree optimization there is usually some scratching of the head followed by the adoption of one of the more obvious ways to work around this issue. Here are some common workarounds:

  1. Turn off tree optimization for the Portal. For smaller portals this might be an option but the larger the portal the more impact it will have on performance. Additionally there is the risk that if the portal functionality grows over multiple iterations you may need to turn tree optimization on again breaking the site map. In reality this is not a recommended option.
  2. Use the View APIs to walk the tree which provide a complete picture of the Portal. The downside with this approach is that the View objects like BookView are not entitled, thus a user can get artifacts in the sitemap they are not entitled to see which can result in confusion. However, if the Portal does not use entitlements this is a perfectly viable option. Finally this option only works for streaming desktops.
  3. Temporarily turn off tree optimization using the _nfto switch. This enables the sitemap to work correctly while the performance is not affected for other pages. The disadvantage is that you need to include _nfto=false on any link pointing to the sitemap page. As a result it limits an administrator's flexibility to modify the Portal layout at runtime since updating links would take a code deployment. Finally, for a large portal you may find performance for the sitemap page significantly hindered by the building of the full control tree.

Of the three options above, the first is a non-starter as turning off tree optimization for the Portal has a whole would seriously impact performance for anything but a trivial Portal. The second two options however are workable particular if we extend the concepts to address the drawbacks presented above.

Thus this article will take an in-depth look at two different options for building a site map. In the first case we will use the presentation context to generate the site map by disabling tree optimization intelligently and automatically so there is no need to worry about manually adding _nfto=false to links. In the second option we will explore the use of the View API in conjunction with manually calculating entitlements to address that limitation.

Before embarking on the specifics of the solutions we first need to discuss the basic architectural elements of the solution that are common to both options.

Basic Solution Architecture

In this section we will cover the basic architectural elements of the solution. A key factor in determining the architecture is that we want to be able to swap different implementations for generating a site map without a lot of effort as well as reuse code where possible. Since the display and caching of the site map is identical between the two solutions it makes sense to isolate the implementation of the two options to the generation of the sitemap itself and share code for everything else.

As a result, we create a basic implementation of an interface called SiteMapFactory that represents the contract for a specific implementation for generating a site map. This interface is quite simple and contains a single method, the definition of the interface is as follows:

/**
 * An interface that defines a factory for building a sitemap.
 */
public interface SiteMapFactory {

	/**
	 * Returns the sitemap for the current request.
	 * @param request
	 * @param response
	 * @return A list of sites.
	 * @throws SiteMapException
	 */
	public SiteMap getSiteMap(HttpServletRequest request, HttpServletResponse response) throws SiteMapException;
}

Now that we have this interface we can go ahead and implement two concrete implementations that represents the two options in question. These implementations are called ContextSiteMapFactory and ViewSiteMapFactory and represent the presentation context and View API options respectively. Each of these implementations will be discussed in more detail in subsequent sections, for now we continue with discussing the common elements shared between these two options.

As per the SiteMapFactory interface, each implementation returns an object implementing the SiteMap interface. The SiteMap interface represents the actual site map and is defined as per below.

public interface SiteMap extends Serializable {

	/**
	 * A list of child sites under this site map.
	 */
	public Iterator getSites();
}

As you can see above, this is a simple interface that simply returns a list of sites for the site map. This list of sites is hierarchical in nature in that each site can in turn have more sites nested within it. Here is the definition of the Site interface.

/**
 * Class that contains information about a single
 * site within a sitemap. Typically classes that implement
 * this interface would be immutable.
 */
public interface Site extends Serializable {

	/**
	 * The URL for the site.
	 */
	public String getUrl();

	/**
	 * The title of the site.
	 */
	public String getTitle();

	/**
	 * The book or page definition label of the
	 * site.
	 */
	public String getLabel();

	/**
	 * Returns true if this site is a book.
	 */
	public boolean isBook();

	/**
	 * A list of child sites under this site.
	 */
	public Iterator getChildren();
}

A basic implementation of the SiteMap and Site interfaces is in the com.bea.ps.sitemap.impl package, these implementations are immutable classes and thus suitable for caching. If you create your own implementations of these interfaces it is highly recommended you follow the same pattern.

With these classes in place we need a way to generate a site map while hiding the specific implementation that is shown. From that description many of you are probably thinking this is a perfect use case for Spring and the Inversion of Control (IOC) pattern. This is absolutely correct, however given that this is an example project it was decided to not use Spring in order to keep it accessible to everyone.

Thus this article does IOC on the cheap in order to accomplish the same goal as we would with Spring. For the purposes of the article, the factory to use to generate the site map within the portlet is specified as a meta property in the portlet's XML definition. Here is an example for the portlet that uses the View API factory.

<?xml version="1.0" encoding="UTF-8"?>
<portal:root
	xmlns:netuix="http://www.bea.com/servers/netuix/xsd/controls/netuix/1.0.0"
	xmlns:portal="http://www.bea.com/servers/netuix/xsd/portal/support/1.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.bea.com/servers/netuix/xsd/portal/support/1.0.0 portal-support-1_0_0.xsd">
	<netuix:portlet definitionLabel="sitemap_1"
		title="View Based SiteMap">
		<netuix:meta name="sitemap.factory" content="com.bea.ps.sitemap.impl.ViewSiteMapFactory"/>
		<netuix:meta name="tree.optimization" content="true"/>
		<netuix:meta name="user.customizable" content="false"/>
		<netuix:titlebar></netuix:titlebar>
		<netuix:content>
			<netuix:jspContent
				contentUri="/portlets/sitemap/sitemap.jsp" />
		</netuix:content>
	</netuix:portlet>
</portal:root>

As you can see from the above, we specified the factory to use in the meta property called sitemap.factory. Using these meta properties allows us to control the behavior of the portlet on a portlet definition by definition basis while still using the same content JSPs for all site map examples. Thus in the example project you will see viewSiteMap.portlet and a contextSiteMap.portlet files in the same directory. These portlets are identical other then the values specified in the meta properties.

In this example project we use this as a convenience technique that allows us to run both portlets side by side so we can easily compare and contrast both approaches. In the real world it is highly recommended to use an IOC container like Spring or an external configuration file to control these properties.

Returning to the portlet definition, notice that there are also two other meta properties specified. The first, tree.optimization, is used to tell the portlet if tree optimization is enabled. While this can be determined from the DesktopDefinition class, it is only available for streaming portals and thus why it is specified here. This property is only used for the presentation context option as we will see later.

The second property is user.customizable. This tells the SiteMapManager if the desktop can be customized by the user and affects how the manager caches the site map. If user customization is not permitted in the desktop then the manager caches the site map globally using the current roles as the caching key. This works because two users with the same set of roles will see an identical site map because entitlements are driven off of roles.

On the other hand, if the desktop is customizable then the desktop is cached in the session since every user could potentially have a different site map. Frankly, this is a naive implementation that really needs to be tailored to the specific scenario. For example, caching in the session could be a poor choice if your portal has many thousands of concurrent users since this would greatly increase memory size. In these cases it might be better to cache the site map globally keyed off the user name with an appropriate limit on cache size.

All of this is to say that you should not assume the caching strategy used in this article will be optimal for all Portal implementations of a site map. Be a good developer and engage your brain on this one rather then thinking this article does all the work for you.

Generating a Site Map with Presentation Contexts

As discussed in the introduction of this article, generating a site map from the presentation contexts would be quite easy if it was not for tree optimization. The root presentation context, DesktopPresentationContext, is easily retrieved from the current HttpServletRequest and then it is just a matter of walking the list of contexts. From the DesktopPresentationContext we can get the child contexts such as the BookPresentationContext and PagePresentationContext where a context exists for each book and page in the Portal.

Here is the method used to walk the presentation contexts recursively.

private void getSites(HttpServletRequest request,
		HttpServletResponse response, BookPresentationContext context,
		List sites) {
	List children = context.getEntitledPagePresentationContexts();
	for (int i = 0; i < children.size(); i++) {
		PagePresentationContext child = (PagePresentationContext) children
				.get(i);
		List childSites = new ArrayList();
		if (child instanceof BookPresentationContext) {
			childSites = new ArrayList();
			getSites(request, response, (BookPresentationContext) child,
					childSites);
		}
		Site site = new SiteImpl(child.getTitle(), child.getLabel(),
				PageURL.createPageURL(request, response, child.getLabel()).toString(),
				childSites.toArray(new Site[childSites.size()]));
		sites.add(site);
	}
}

With tree optimization turned on we can only get the presentation contexts for the slice of the tree that is currently active. As a result, we need to turn off the tree optimization for the single request where the site map needs to be generated. To handle this issue we will attach a backing file to the context site map portlet. Thus if tree optimization is on the backing file simply redirects the request while including _nfto=false in the redirected request. This ensures the next request will have tree optimization turned off and permits a full site map to be constructed.

The backing file does need to perform some checks prior to doing a redirect, the render method will check to see if a site map needs to be generated and if so it also checks if tree optimization is on. Another check is seeing if the site map is already cached, after all there is no point in redirecting the request with tree optimization off if all that is needed is to pull the site map from the cache.

Once tree optimization is turned off building the site map itself off of the presentation contexts is quite simple to implement. However this technique of redirecting does have a couple of potential drawbacks that need to be highlighted. If the site map portlet is sharing the page with other portlets the redirect could interfere with the proper functioning of other portlets. Also, if other portlets on the page require a significant amount of time to render the redirect could cause reduced performance in handling two requests instead of one.

Generating a Site Map with the View API

This approach discusses the use of the View APIs to build a site map. Unlike the presentation context option, this option has no issues with tree optimization. However it does have a different issue, namely that the View APIs are not entitled. This means that if we use the View APIs as is the site map would show all books and pages including those that are entitled and the user cannot access. The solution we present here corrects this oversight by using the entitlements API to calculate the entitlement for each view artifact correctly.

The first thing that the code does is to get an instance of the DesktopView class that represents the current desktop, here is the code for that.

PortalCustomizationManager manager = PortalBeanManager
    .getInstance().getPortalCustomizationManager();
CustomizationContext cc = new CustomizationContext(request);
DesktopView view = manager.getDesktopView(cc, appContext
    .getWebAppName(),
    new PortalPath(appContext.getPortalPath()),
    new DesktopPath(appContext.getDesktopPath()));

Once we have the DesktopView it is an easy matter to get the root BookView and then start building the tree. The BookView interface declares a method called getNavigableViews() and this is what we use to get the children of the BookView.

Once we have a specific View object, be it BookView or PageView, we need to check if is entitled and this is where things get complicated. For better or worse the documentation around the Portal security framework is pretty weak in terms of explaining how things work but the key class is the Authorization class.

On the positive side, if you look at the javadoc for that class you can see that it has a method called isAccess allowed which fits our need nicely. On the negative side, it takes an object called a P13nResource object which is turn built from a string called a ResourceID. The negative is what these ResourceID strings consist of is not documented as far as I know, however a little digging around shows that they are a concatenated String with each element separated by the constant EntitlementConstants.RESOURCE_ID_DELIMITER.

In the first attempt at getting this going, the class HierarchyRoleResource which extends P13nResource, was used. Creating one of these instances is quite straightforward and can be done as follows:

String resourceId = resourceType
    + EntitlementConstants.RESOURCE_ID_DELIMITER
    + appContext.getPortalPath()
    + EntitlementConstants.RESOURCE_ID_DELIMITER
    + appContext.getDesktopPath()
    + EntitlementConstants.RESOURCE_ID_DELIMITER
    + view.getDefinitionLabel();

P13nResource resource = new HierarchyRoleResource(SecurityHelper
    .getApplicationName(), appContext.getWebAppName(), resourceId,
    "view");

Now you may be wondering, how did we figure out how to build the resourceID variable? Fortunately there is an easy way in Portal to find out the Resource IDs needed and that is the propagation tool. The propagation tool needs to output entitlements in order to be able to create them in a new environment. By reviewing the propagation inventory, which is simply a collection of XML files, we can extract the format for the Resource ID.

To get a sample ID, simply create an entitlement in the Portal Admin Tool (PAT) and then create a propagation inventory. Open the inventory zip file and then open the toc_0.zip contained within that. Finally open the toc.xml file in an editor and search for '__Entitlement'. You should see something like this:

<node>
    <name>Page__test__test__ptlFinanceManagement__view__Entitlement</name>
    <type>SEC_PF_ENTITLEMENT_INSTANCE_NODE</type>
    <taxonomy>Application:portalservices:portalWAR.WebApp:test.Portal:test.Desktop:test.DefaultDesktop:test_portal_book_1.MainBook:bkFinance.BookMember:bkFinance.Book:ptlFinanceManagement.BookMember:ptlFinanceManagement.Page:EntitlementService:Page__test__test__ptlFinanceManagement__view__Entitlement</taxonomy>
    <filename>Page__test__test__ptlFinanceManagement__view__Entitlement.entitlement</filename>
</node>

The resource ID is everything up to __view and by replacing the double underscores with the RESOURCE_ID_DELIMITER we can easily figure out how to construct the resource ID in a generic fashion.

So now that we have the Resource ID down, we simply need to call the isAccess method as follows:

return Authorization.isAccessAllowed(resource, ctxHandler);

On the first test, everything looked good and any book or page instances that were entitled did not appear in the site map. However after more testing it was found that if the entitlement was at the library level it still showed in the site map, something was not quite right. Further investigation revealed that we needed to use the P13nLibraryResource class instead of the HierarchyRoleResource. This also meant we needed a library resource ID as well, fortunately using the same propagation technique explained earlier the format for the library resource was easily discernable. Here is what the code looks like after these changes:

String resourceId = resourceType
    + EntitlementConstants.RESOURCE_ID_DELIMITER
    + appContext.getPortalPath()
    + EntitlementConstants.RESOURCE_ID_DELIMITER
    + appContext.getDesktopPath()
    + EntitlementConstants.RESOURCE_ID_DELIMITER
    + view.getDefinitionLabel();

String libraryId = "com_bea_p13n"
    + EntitlementConstants.RESOURCE_ID_DELIMITER + resourceType
    + EntitlementConstants.RESOURCE_ID_DELIMITER
    + view.getDefinitionLabel();

P13nResource resource = new P13nLibraryResource(SecurityHelper
    .getApplicationName(), appContext.getWebAppName(), resourceId,
    "view", libraryId);

return Authorization.isAccessAllowed(resource, ctxHandler);

Subsequent testing showed that everything works as expected regardless if the entitlement is at the library or instance level.

One last item remains though, a concern is performance as checking entitlements can be an expensive proposition and if possible we should cache the results. Fortunately we can take advantage of the fact that entitlements are leveraged off of roles, if two users have the same combination of roles then they should have the same sitemap.

The SiteManager class does exactly this and caches the sitemap for a given set of roles in a Portal cache. In order to get all of the user's roles we must use the Portal APIs so that the Portal visitor roles are also included. Here is the method that does this.

private static String[] getAllRoles(HttpServletRequest request) throws EntitlementsException {
    AppContext appContext = AppContext.getAppContext(request);
    P13nContextHandler ctxHandler = EntitlementHelper
        .getP13nContextHandler(request);

    HierarchyRoleResource resource = new HierarchyRoleResource(SecurityHelper
        .getApplicationName(), appContext.getWebAppName(), EntitlementConstants.P13N_ROLE_POLICY_POOL, "");
    Map map = Authorization.getRoles(resource, ctxHandler);
    return (String[])map.keySet().toArray(new String[map.size()]);
}

One drawback to this caching is that we are assuming the desktop is not customized on a per user basis. If the Portal you are developing supports desktop customization then make sure to disable the caching in the code as there is little benefit to caching the sitemap on a per user basis.

With that we have a facility for providing an efficient and performant site map and learned more about how entitlements worked under the hood. A nice little two for one deal.

WebLogic Portal 10.2 Optimization

One note about the View APIs, there is an optimization that was added to WLP 10.2 that causes the Portal servlet to place the DesktopView in the request as an attribute. This can be beneficial because the View heirarchy can be expensive to generate with many database calls. If a copy is already available then there is no need for the site map portlet to build it's own copy.

The problem with this from a portlet perspective is that the copy is only available on the first request the Portal servlet needs to fetch the View tree. Typically the user would go to the Portal home page on the first request, where the DesktopView would be available, and then go to the site map on a subsequent request with the DesktopView object no longer being available.

In cases where the desktop is not customizable it makes sense to take advantage of the availability of the DesktopView and pre-load the sitemap in advance. Since the sitemap is cached globally, pre-loading it avoids taking a second unnecessary hit building the view object for the first user who accesses it. This can be easily accomplished by creating a backing file for the Portal desktop that checks if the DesktopView is in the request and then gets a site map in order to force it to be generated and cached.

In cases where the user desktop is customizable, you have to determine if pre-loading the site map is worthwhile since it would be cached on a per user basis. Many users will not access the site map over the course of their session and having the site map cached for each concurrent user may be onerous in terms of memory usage.

In either case, one other useful feature of this attribute is that the presence of the DesktopView in the request also signals that the desktop has changed. This means it could be used to clear the cache entry for given site map in order to ensure it is regenerated for the new Desktop layout. An example of this is if the user or administrator customizes the desktop.

An implementation of this desktop backing file is left as an exercise to the reader.

Sample Code

This tutorial includes an archive that contains a sample WebLogic Portal application with all the resources needed to demonstrate a site map. To use this sample, simply unpack the archive, ensuring that paths are preserved, and then import the projects into a new Workspace. Deploy the application to a server and then create a streaming desktop in the Portal Admin Tool.

Once you have created a streaming desktop, try creating an entitlement at both the library and instance level and confirm that the site map is being generated correctly with respect to those entitlements.

Summary

Building an efficient and effective site map in WebLogic Portal can be challenging but with a bit of work that challenge can be overcome.

Posted by Gerald Nunn | |