Within WCM, content can be categorized. Normally, this is used to allow WCM menus to display content related to specific categories. However, it is also possible for users within WCM to have categories associated with their profile. This can be done within the
. Once the user is associated with categories, a WCM menu can then retrieve content that is associated with the same categories as the user.
In this example, we will show how these categories can be used within PZN rules to retrieve the content that is related to the categories that the user is interested in.
One note, the APAR PM75476 is necessary for this example to work if the WCM Dynamic Mapper is being used.
There are 2 approaches that we can use for this application object. One is to retrieve the values that WCM has associated with the user. This is useful if the end user has categories stored within WCM, the dynamic mapper approach is being used (categories added to WCM Users programmatically), or the end user is selecting their own categories using the taxonomy tree. The other approach is that the custom application object can associate categories for the specific user programmatically. This approach is useful, because categories can selected based on things like user cookies, request attributes in the current request, etc. This particular example will retrieve the categories directly from WCM for the user.
Note, we have included these assets as a .jar file already which can be used directly as downloaded. These steps are included for reference, they can be skipped by downloading the customcategoryapp.jar file, placing in /WPHOME/pzn/prereq.pzn/lib directory and restarting the Portal server. Just skip to the section "Create the Application Object within PZN UI".
I used Rational Application Developer for this process. If you are using a different IDE, your steps may be different.
One piece of the functionality of this custom application object is a WCM Category Cache. The purpose of this is to allow the custom application object to retrieve WCM category information without using WCM API retrievals for every request. At a high level, it basically caches the categories with a variety of keys, including title and path to the category, and returns the UUID of the category for use by the PZN rules.
package com.ibm.wcm.sample.categorycache;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.DuplicateKeyException;
import javax.naming.InitialContext;
import com.ibm.websphere.asynchbeans.Alarm;
import com.ibm.websphere.asynchbeans.AlarmListener;
import com.ibm.websphere.asynchbeans.AlarmManager;
import com.ibm.websphere.asynchbeans.AsynchScope;
import com.ibm.websphere.asynchbeans.WorkManager;
import com.ibm.workplace.wcm.api.Category;
import com.ibm.workplace.wcm.api.DocumentId;
import com.ibm.workplace.wcm.api.DocumentIdIterator;
import com.ibm.workplace.wcm.api.DocumentLibrary;
import com.ibm.workplace.wcm.api.DocumentTypes;
import com.ibm.workplace.wcm.api.Taxonomy;
import com.ibm.workplace.wcm.api.WCM_API;
import com.ibm.workplace.wcm.api.Workspace;
import com.ibm.workplace.wcm.api.exceptions.WCMException;
public class WCMCategoryUtils {
private static final Logger s_log = Logger.getLogger(WCMCategoryUtils.class.getName());
/**
* Set this to the number of minutes inbetween rebuilds of the categoryId Map.
*/
private static final int REBUILD_INTERVAL_MINUTES = 60;
private static AlarmManager s_alarmManager = null;
private static Alarm s_lastAlarm = null;
private static Map<String, List<CategoryInformation s_categoryNamesIDs = new HashMap<String, List<CategoryInformation ();
private static AlarmListener s_alarmListener = new AlarmListener()
{
public void fired(Alarm p_alarm)
{
if (s_log.isLoggable(Level.FINER))
{
s_log.finer("Alarm fired - will rebuild the category map.");
}
// This is to try and keep a single rebuild alarm. If the EAR/WAR is redeployed the previous alarm
// is not cancelled. So see if the alarm firing now is the one scheduled form this load of the class
// if it is the same we reschedule, if not, we can ignore it....
boolean rescheduleAlarm = s_lastAlarm != null;
if ( rescheduleAlarm )
{
rescheduleAlarm = s_lastAlarm.equals( p_alarm );
}
// Rebuild the Map, and make sure we reschediule the next rebuild.
WCMCategoryUtils.buildCategoryIdsMap(rescheduleAlarm);
}
};
static
{
// Now schedule the next time to rebuild
// Lookup the AlarmManager, so we can schedule rebuild tasks
if (s_alarmManager == null)
{
try
{
InitialContext context = new InitialContext();
// WorkManager wm = (WorkManager)context.lookup("java:comp/env/wm/default");
WorkManager wm = (WorkManager) context.lookup("wm/default");
AsynchScope ascope;
String scopeName = "WCMCategoryUtils_AScope";
try
{
ascope = wm.createAsynchScope(scopeName);
}
catch (DuplicateKeyException ex)
{
ascope = wm.findAsynchScope(scopeName);
s_log.warning(ex.getMessage());
}
// get an AlarmManager
s_alarmManager = ascope.getAlarmManager();
}
catch (Exception e)
{
s_log.log(Level.SEVERE, "Unable to schedule rebuild of CategoryIDS", e);
}
}
buildCategoryIdsMap(true);
}
public static void rebuildCategoryIdsMap()
{
// When invoked directly do not rescheuld otherwise we'll end up with multiple scheduled tasks.
buildCategoryIdsMap(false);
}
private static void buildCategoryIdsMap(boolean rescheduleRebuild)
{
if (s_log.isLoggable(Level.FINER))
{
s_log.entering("WCMCategoryUtils", "buildCategoryIdsMap");
}
Map<String, List<CategoryInformation categoryIdsMap = new HashMap<String, List<CategoryInformation ();
try
{
Workspace sysWorkspace = WCM_API.getRepository().getSystemWorkspace();
DocumentLibrary currentLibrary = null;
sysWorkspace.login();
try
{
currentLibrary = sysWorkspace.getCurrentDocumentLibrary();
// iterate All document libraries
Iterator allLibs = sysWorkspace.getDocumentLibraries();
while (allLibs.hasNext()) {
DocumentLibrary designLibrary = (DocumentLibrary) allLibs
.next();
String libName = designLibrary.getName();
sysWorkspace.setCurrentDocumentLibrary(designLibrary);
DocumentIdIterator idIter = sysWorkspace.findByType(
DocumentTypes.Taxonomy,
Workspace.WORKFLOWSTATUS_PUBLISHED);
while (idIter.hasNext()) {
DocumentId docid = idIter.nextId();
Taxonomy tax = (Taxonomy) sysWorkspace.getById(docid,
true);
String fullPath = tax.getName();
DocumentIdIterator catIter = tax.getChildren();
while (catIter.hasNext()) {
DocumentId catId = catIter.nextId();
Category cat = (Category) sysWorkspace.getById(
catId, true);
computeChildPath(libName, sysWorkspace, categoryIdsMap, cat,
fullPath);
}
}
}
if (s_log.isLoggable(Level.FINE))
{
s_log.fine("Finished computing Category mapping information for a total of " + (categoryIdsMap.size() / 5) + "categories");
if (s_log.isLoggable(Level.FINER))
{
s_log.finer("Computed the following keys for the Categories");
for (String key : categoryIdsMap.keySet())
{
s_log.finer(" " + key);
}
try
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(categoryIdsMap);
oos.flush();
NumberFormat intFormatter = NumberFormat.getIntegerInstance();
s_log.finer("Computed Size of the entire Map is " + intFormatter.format(baos.toByteArray().length) + " bytes");
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}
finally
{
if (currentLibrary != null)
sysWorkspace.setCurrentDocumentLibrary(currentLibrary);
sysWorkspace.logout();
}
}
catch (WCMException e)
{
}
// Now assign the new map to the static var
s_categoryNamesIDs = categoryIdsMap;
// If we resolved the alaram Manager, schedule the rebuild.
if (rescheduleRebuild && s_alarmManager != null)
{
s_lastAlarm = s_alarmManager.create(s_alarmListener, s_alarmListener, REBUILD_INTERVAL_MINUTES * 60 * 1000);
}
if (s_log.isLoggable(Level.FINER))
{
s_log.exiting("WCMCategoryUtils", "buildCategoryIdsMap");
}
}
static void computeChildPath(String libraryName, Workspace sysWorkspace, Map<String, List<CategoryInformation categoryIdsMap, Category cat, String path)
{
String categoryName = cat.getName();
DocumentId catId = cat.getId();
String catIdString = catId.getId();
path = path + "/" + categoryName;
List<CategoryInformation> catInfoList = categoryIdsMap.get(categoryName);
if (catInfoList == null)
{
catInfoList = new ArrayList<CategoryInformation>(1);
}
CategoryInformation catInfo = new CategoryInformation();
catInfo.setCategoryName(categoryName);
catInfo.setCategoryFullPath(path);
catInfo.setCategoryId(catIdString);
catInfoList.add(catInfo);
List<CategoryInformation> catInfoPathList = new ArrayList<CategoryInformation>(1);
catInfoPathList.add(catInfo);
categoryIdsMap.put(categoryName, catInfoList);
categoryIdsMap.put(libraryName + "/" + categoryName, catInfoList);
categoryIdsMap.put(path, catInfoPathList);
categoryIdsMap.put(libraryName + "/" + path, catInfoPathList);
// add a full lower case path as well
String loweredpath = libraryName + "/" + path;
categoryIdsMap.put(loweredpath.toLowerCase(), catInfoPathList);
categoryIdsMap.put(catIdString, catInfoPathList);
// add a full lower case path as well
String encodedPath = "";
try
{
encodedPath = sysWorkspace.getPathById(catId, false, false);
}
catch (Exception e)
{
e.printStackTrace();
DocumentIdIterator catIter = cat.getChildren();
while (catIter.hasNext())
{
DocumentId childCatId = catIter.nextId();
try
{
Category childCat = (Category) sysWorkspace.getById(childCatId, true);
computeChildPath(libraryName, sysWorkspace, categoryIdsMap, childCat, path);
}
catch (WCMException e)
{
e.printStackTrace();
}
}
return;
}
public static String getCategoryId(String categoryName)
{
String categoryId = null;
CategoryInformation catInfo = getCategoryInformation(categoryName);
if (catInfo != null)
{
categoryId = catInfo.getCategoryId();
}
return categoryId;
}
public static List<CategoryInformation> getCategoryInformations(String categoryNameOrId)
{
return s_categoryNamesIDs.get(categoryNameOrId);
}
public static CategoryInformation getCategoryInformation(String categoryNameOrId)
{
if (s_log.isLoggable(Level.FINER))
{
s_log.entering("WCMCategoryUtils", "getCategoryInformation", categoryNameOrId);
}
CategoryInformation catInfo = null;
List<CategoryInformation> catInfoList = s_categoryNamesIDs.get(categoryNameOrId);
if (catInfoList != null)
{
catInfo = catInfoList.get(0);
}
if (s_log.isLoggable(Level.FINER))
{
s_log.exiting("WCMCategoryUtils", "getCategoryInformation", catInfo);
}
return catInfo;
}
public static String getCategoryPath(String categoryId)
{
String categoryPath = null;
CategoryInformation catInfo = getCategoryInformation(categoryId);
if (catInfo != null)
{
categoryPath = catInfo.getCategoryFullPath();
}
return categoryPath;
}
public static String convertQueryStringToFullPath(String queryString, String libraryName)
{
if (s_log.isLoggable(Level.FINER))
{
s_log.entering("WCMCategoryUtils", "convertQueryStringToFullPath", new String[] { queryString, libraryName });
}
StringBuilder builder = new StringBuilder();
String[] categoryNames = queryString.split(",");
for (String categoryName : categoryNames)
{
List<CategoryInformation> categoryInfos = getCategoryInformations(categoryName);
if ( categoryInfos != null && categoryInfos.size() > 0 ) {
for (CategoryInformation info : categoryInfos)
{
String path = info.getCategoryFullPath();
if (path != null)
{
if (libraryName != null)
{
builder.append(libraryName);
builder.append("/");
}
builder.append(path);
builder.append(",");
}
}
}
}
// Trim off the trailing comma
if (builder.length() > 0)
{
builder.setLength(builder.length() - 1);
}
if (s_log.isLoggable(Level.FINER))
{
s_log.exiting("WCMCategoryUtils", "convertQueryStringToFullPath", builder.toString());
}
return builder.toString();
}
public static List<String> getCategoryIds(String categoryName) throws WCMException
{
return getCategoryIds(categoryName, false);
}
public static List<String> getCategoryIds(String categoryName, boolean includeParents) throws WCMException
{
if (s_log.isLoggable(Level.FINER))
{
s_log.entering("WCMCategoryUtils", "getCategoryIds", new String[] { categoryName, "includeParents=" + includeParents });
}
Set<String> categoryIds = null;
List<CategoryInformation> categoryInfos = getCategoryInformations(categoryName);
if (categoryInfos != null && categoryInfos.size() > 0)
{
categoryIds = new HashSet<String>(categoryInfos.size());
for (CategoryInformation info : categoryInfos)
{
categoryIds.add(info.getCategoryId());
if (includeParents)
{
boolean keepFindingParents = true;
// Strip off the taxonomy part.
// String path =
// info.getCategoryFullPath().substring(info.getCategoryFullPath().indexOf("/")+1);
String path = info.getCategoryFullPath();
while (keepFindingParents)
{
int index = path.lastIndexOf("/");
if (index > 0)
{
path = path.substring(0, index);
if (!categoryIds.contains(path))
{
String catid = getCategoryId(path);
if (catid != null)
{
categoryIds.add(getCategoryId(catid));
}
else
{
keepFindingParents = false;
}
}
else
{
keepFindingParents = false;
}
}
else
{
keepFindingParents = false;
}
}
}
}
}
List<String> ids = new ArrayList<String>(categoryIds.size());
ids.addAll(categoryIds);
if (s_log.isLoggable(Level.FINER))
{
s_log.exiting("WCMCategoryUtils", "getCategoryIds", ids);
}
return ids;
}
}
Now, follow the above process and create a class named UserCategoriesObject in the package com.ibm.websphere.personalization.custom.appobject. Enter the following for the code:
package com.ibm.websphere.personalization.custom.appobject;
import java.security.Principal;
import java.util.*;
import com.ibm.portal.um.PumaHome;
import com.ibm.portal.um.PumaLocator;
import com.ibm.portal.um.User;
import com.ibm.portal.um.exceptions.PumaException;
import com.ibm.workplace.wcm.api.*;
import com.ibm.wcm.sample.categorycache.*;
import com.ibm.websphere.personalization.RequestContext;
import com.ibm.websphere.personalization.applicationObjects.SelfInitializingApplicationObject;
import com.ibm.websphere.personalization.context.PersonalizationContext;
import java.io.Serializable;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.logging.Level;
import java.util.logging.Logger;
public class UserCategoriesObject implements SelfInitializingApplicationObject, Serializable
{
private static final Logger s_log = Logger.getLogger(UserCategoriesObject.class.getName());
private Workspace workspace;
private static PumaHome puma_home = null;
private String currentCategoriesUuidString = "";
public static String UUID_PREFIX = "uuid:";
public List currentCategories;
/**
* @return the currentCategoriesString
*/
public String getCurrentCategoriesUuidString()
{
if (s_log.isLoggable(Level.FINEST))
{
s_log.log(Level.FINEST, "getCurrentCategoriesUuidString returning " + currentCategoriesUuidString);
}
return currentCategoriesUuidString;
}
/**
* @param currentCategoriesUuidString the currentCategoriesString to set
*/
public void setCurrentCategoriesUuidString(String currentCategoriesUuidString)
{
if (s_log.isLoggable(Level.FINEST))
{
s_log.log(Level.FINEST, "setCurrentCategoriesUuidString called with " + currentCategoriesUuidString);
}
this.currentCategoriesUuidString = currentCategoriesUuidString;
}
/**
* @param currentCategories the currentCategories to set
*/
public List getCurrentCategories()
{
return currentCategories;
}
/* (non-Javadoc)
* @see com.ibm.websphere.personalization.applicationObjects.SelfInitializingApplicationObject#init(com.ibm.websphere.personalization.RequestContext)
* In this init, we will basically check to see if the value we wish
* to use is null, and if so set it
*/
public void init(RequestContext rc)
{
boolean isFinest = s_log.isLoggable(Level.FINEST);
// TODO Auto-generated method stub
// check if the current cats are null. If so, set them;
if (isFinest)
{
s_log.entering(this.getClass().getName(), "init");
}
PersonalizationContext pc = (PersonalizationContext) rc;
if (s_log.isLoggable(Level.FINEST))
{
s_log.log(Level.FINEST, "getCurrentCategories() null");
}
ArrayList holdingList = new ArrayList();
// generate a WCM workspace and render WCM content
// have to pull the current user from puma
if (puma_home == null)
{
getPumaHome();
}
try
{
User thisUser = puma_home.getProfile().getCurrentUser();
InitialContext ctx = new InitialContext();
// Retrieve WebContentService using JNDI name
WebContentService webContentService = (WebContentService) ctx.lookup("portal:service/wcm/WebContentService");
workspace = webContentService.getRepository().getWorkspace(thisUser);
if (isFinest)
{
s_log.log(Level.FINEST, "workspace retrieved for " + thisUser.toString());
}
UserProfile currentUserProfile = workspace.getUserProfile();
String[] currentCats = currentUserProfile.getCategories();
if (isFinest)
{
s_log.log(Level.FINEST, "currentCats was length " + currentCats.length);
if (currentCats.length > 0)
{
for (int x = 0; x < currentCats.length; x++)
{
s_log.log(Level.FINEST, "currentCats contained " + currentCats[x]);
}
}
}
String tempCatString = "";
String fullCatsUuidString = "";
String holdingString = null;
for (int x = 0; x < currentCats.length; x++)
{
tempCatString = currentCats[x];
// ditch the leading /
if (tempCatString.charAt(0) == '/')
{
tempCatString = tempCatString.substring(1);
}
// lower it
tempCatString = tempCatString.toLowerCase();
holdingString = null;
holdingString = WCMCategoryUtils.getCategoryId(tempCatString);
if (holdingString != null)
{
tempCatString = UUID_PREFIX + holdingString;
fullCatsUuidString = fullCatsUuidString + tempCatString;
if (x < currentCats.length - 1)
{
fullCatsUuidString = fullCatsUuidString + ",";
}
}
}
// if the fullCatsString has trailing , remove
if (fullCatsUuidString.endsWith(","))
{
fullCatsUuidString = fullCatsUuidString.substring(0, fullCatsUuidString.length() - 1);
}
setCurrentCategoriesUuidString(fullCatsUuidString);
}
catch (Exception e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
// set myself in the session
// that way, won't have to reinit
pc.setSessionAttribute("CurrentCats", this);
if (isFinest)
{
s_log.exiting(this.getClass().getName(), "init");
}
}
/**
*
* getPumaHome helper method to retrieve pumahome
* @return
*/
PumaHome getPumaHome()
{
if (puma_home == null)
{
try
{
Context ctx = new InitialContext();
puma_home = (PumaHome) ctx.lookup(PumaHome.JNDI_NAME);
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
return puma_home;
}
}
At a high level, this code retrieves the current user from PUMA SPI, creates a WCM workspace for that user, and then retrieves that user's categories. These categories are returned as path values, such as /LibraryName/Taxonomy/path/to/category. When PZN rules use categories, they use the uuid of the categories, so we have to convert the path to the category to a uuid value. Also, the rule expects the UUID to be passed in as comma delimited list of uuid values, with "uuid:" prepended to them. So this custom code does just this. Now, when the custom application object is referenced, it will retrieve the current user categories by retrieving the string using the getCurrentCategoriesUuidString method.
Now that the custom application object code is in place, we need to deploy to the WebSphere Portal server. In order for custom application objects to be used by Personalization, the code has to be placed in /WPHOME/pzn/prereq.pzn/lib.
Now that the code is in place, in order to use the UserCategoriesObject, we have to create the application object within the PZN UI.
To display the results, we will create a WCM PZN Component in order to reference the rule.
Now that we have the WCM Component in place, we just have to reference it from a WCM Rendering Portlet.