Example 2: Filter class
This sample filter generates the friendly URL.
The filter runs several steps to create the friendly URL:
- The filter determines the target portal page from one of the following sources:
- The configuration of the web content viewer.
- Any web content pages that have a content association to the content for which the URL is generated.
- A target page that is specified by the
UrlCmpnt
tag.
- If a target page is identified, the filter verifies that the page is a web content page with a content association. The filter then validates that the content for which the URL is generated is a child of the site area that is mapped to the page. If the content to render is not a child of the site area that is associated with the page, the filter writes a new URL.
- The filter then writes the friendly URL by combining the following information:
- The friendly URL name of the target page.
- The path to the content, relative to the site area that is associated with the target page.
/******************************************************************
* Copyright HCL Technologies Limited 2011, 2014, 2019 *
******************************************************************/
package com.ibm.workplace.wcm.api.samples;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.logging.*;
import java.util.regex.*;
import javax.portlet.*;
import com.ibm.portal.*;
import com.ibm.portal.content.*;
import com.ibm.portal.identification.*;
import com.ibm.portal.mappingurl.*;
import com.ibm.portal.resolver.friendly.*;
import com.ibm.portal.resolver.friendly.accessors.url.*;
import com.ibm.portal.resolver.friendly.helper.*;
import com.ibm.portal.resolver.friendly.service.*;
import com.ibm.portal.serialize.*;
import com.ibm.portal.services.contentmapping.*;
import com.ibm.portal.services.contentmapping.exceptions.*;
import com.ibm.portal.state.*;
import com.ibm.portal.state.accessors.selection.*;
import com.ibm.portal.state.exceptions.*;
import com.ibm.workplace.wcm.api.*;
import com.ibm.workplace.wcm.api.exceptions.*;
import com.ibm.workplace.wcm.api.extensions.url.*;
import com.ibm.workplace.wcm.api.extensions.url.PortletContextSharingConfig.PublishConfig;
/**
* Content URL generation filter that tries to generate stateless friendly URLs
* for web content pages.
*
* The filter that writes the friendly URL does the following steps to generate
* the friendly URL
*
* <ol>
* <li>It determines the target portal page from one of the following sources The
* Web Content Viewer configuration Web content pages that have a content
* mapping for the content the URL is generated for A target page specification
* from the WCM [UrlCmpnt] tag
* </li>
* <li>If a page could be determined it checks if the page is a web content page
* i.e. if the page has a content mapping assign. It then validates that the
* content the URL is generated for is a children of the site area mapped to the
* page. In case the content is not a children of the site area mapped to the
* page new URL is written by this filter.
* </li>
* <li>Finally the friendly URL is written that is build from the friendly URL
* name of the target page appended with the content path relative to the site
* area mapped to the target page.</li>
* </ol>
* <p>
* <b>Note:</b> In order to use the following sample filter all pages a content URL is
* generated for need to be web content pages with a friendly name assigned and
* a default content mapping that points to a parent of the content.</p>
*/
public class FriendlyUrlGenerationFilter implements ContentUrlGenerationFilter {
/** logger */
private static final Logger LOGGER = Logger.getLogger(FriendlyUrlGenerationFilter.class.getName());
/** the path separator / */
private static final String PATH_SEPARATOR = "/";
/** regular expression pattern to split a path into segments */
private static final Pattern PATH_SEPARATOR_PATTERN = Pattern.compile(PATH_SEPARATOR);
/** friendly selection service */
private final FriendlySelectionService friendlySelectionService;
/** content model */
private final ContentModel<ContentNode> contentModel;
/** WCM workspace */
private final Workspace workspace;
/** identifcation service */
private final Identification identification;
/** state manager */
private final PortletStateManager stateManager;
/** url mapping model */
private final MappingURLTreeModel urlMappingModel;
/** content mapping info home */
private final ContentMappingInfoHome contentMappingInfoHome;
/** selection accessor */
private final SelectionAccessorFactory selectionFactory;
/** factory for friendly URLs */
private final FriendlyURLFactory friendlyUrlFactory;
/** the currently selected page */
private ObjectID currentPage;
/**
* Create a new filter instance. This should be called once per render
* request
*
* @param friendlySelectionService
* The friendly selection service
* @param contentModel
* The content model
* @param workspace
* The WCM workspace
* @param identification
* The identificaton service
* @param stateManager
* The state manager service
* @param urlMappingTreeModel
* The url mapping model
* @param contentMappingInfoHome
* The content mapping home interface
*
* @throws CannotInstantiateAccessorException
* If instantiation of state selection accessor factory fails
* @throws UnknownAccessorTypeException
* If instantiation of state selection accessor factory fails
*/
public FriendlyUrlGenerationFilter(final FriendlySelectionService friendlySelectionService,
final ContentModel<ContentNode> contentModel, final Workspace workspace,
final Identification identification, final PortletStateManager stateManager,
final MappingURLTreeModel urlMappingModel, final ContentMappingInfoHome contentMappingInfoHome)
throws UnknownAccessorTypeException, CannotInstantiateAccessorException {
final boolean isLogging = LOGGER.isLoggable(Level.FINEST);
if (isLogging) {
LOGGER.entering(getClass().getName(), "<init>", new Object[] { friendlySelectionService, contentModel,
workspace, identification, stateManager, urlMappingModel, contentMappingInfoHome });
}
this.friendlySelectionService = friendlySelectionService;
this.friendlyUrlFactory = friendlySelectionService.getURLFactory();
this.contentModel = contentModel;
this.workspace = workspace;
this.identification = identification;
this.stateManager = stateManager;
this.urlMappingModel = urlMappingModel;
this.contentMappingInfoHome = contentMappingInfoHome;
this.selectionFactory = stateManager.getAccessorFactory(SelectionAccessorFactory.class);
if (isLogging) {
LOGGER.exiting(getClass().getName(), "<init>");
}
}
@Override
public void dispose() {
final boolean isLogging = LOGGER.isLoggable(Level.FINEST);
if (isLogging) {
LOGGER.entering(getClass().getName(), "dispose");
}
// dispose all request specific services
this.friendlySelectionService.dispose();
this.stateManager.dispose();
if (isLogging) {
LOGGER.exiting(getClass().getName(), "dispose");
}
}
@Override
public void writeURL(final ContentUrlGenerationRequest request, final ContentUrlGenerationResponse response,
final ContentUrlGenerationFilterChain chain) throws ContentUrlGenerationException, IOException {
final boolean isLogging = LOGGER.isLoggable(Level.FINEST);
if (isLogging) {
LOGGER.entering(getClass().getName(), "writeURL", new Object[] { request.getContentPath(false) });
}
// as we need to use the path to lookup the item in WCM we need the
// decoded version
final String contentPath = request.getContentPath(false);
if (contentPath != null) {
// Check if we should generate a URL that publishes to the
// current
// or another page or uses the dynamic publishing
final PortletContextSharingConfig ctxSharingConfig = request.getPortletContextSharingConfig();
final PublishConfig publishConfig = ctxSharingConfig.getPublishConfig();
final PortletRequest portletRequest = request.getPortletRenderRequest();
final PortletResponse portletResponse = request.getPortletRenderResponse();
ObjectID targetPageId = null;
try {
// determine the target page. The target page is determined
// from either a dynamic target page override (i.e. on the
// UrlCmpnt tag), a web content mapping on a page or from the
// portlet configuration
// check if a dynamic page target as been set as it can be
// set on the WCM UrlCmpnt tag
final TargetPageConfig targetPageDynamic = request.getDynamicTargetPageOverride();
if (targetPageDynamic != null) {
// lookup the page from the dynamic target page override
targetPageId = getTargetPage(portletRequest, portletResponse, targetPageDynamic);
} else {
if (publishConfig.getMode() == PublishConfig.MODE_DYNAMIC) {
// lookup the target page from content mappings
targetPageId = lookupTargetPage(portletRequest, portletResponse, contentPath);
} else {
// target page is determined from portlet
// configuration
final TargetPageConfig targetPagePortletConfig = publishConfig.getTargetPage();
if (targetPagePortletConfig != null) {
// lookup the page from the portlet target page
// configuration
targetPageId = getTargetPage(portletRequest, portletResponse, targetPagePortletConfig);
}
}
}
if (targetPageId != null) {
// check if the path of the content is a children of the
// site area mapped to the page and get the path relative to
// this site area
final String relativePathInfo = getRelativePathInfo(contentPath, targetPageId);
if (relativePathInfo != null) {
// write the friendly URL to the page and the
// relative path information added
final FriendlyURL url = this.friendlyUrlFactory
.newURL(com.ibm.portal.state.Constants.Clone.EMPTY_COPY);
url.setSelection(targetPageId);
if(!relativePathInfo.isEmpty()) {
url.setPathInfo(relativePathInfo);
}
url.writeDispose(response.getWriter());
} else {
if (isLogging) {
LOGGER.logp(Level.FINEST, getClass().getName(), "writeURL",
"Content [{0}] is not a children of the site area mapped to page with ID [{1}]",
new Object[] { contentPath, targetPageId });
}
// the content is not a children of the site area
// mapped to the target page so forward the request to
// the chain of URL
chain.writeURL(request, response);
}
} else {
if (isLogging) {
LOGGER.logp(Level.FINEST, getClass().getName(), "writeURL",
"No target page could be determined for content [{0}]", new Object[] { contentPath });
}
// no target page could be determined
// let the content URL generation chain handle the
// request
chain.writeURL(request, response);
}
} catch (SerializationException e) {
throw new ContentUrlGenerationException(e);
} catch (ModelException e) {
throw new ContentUrlGenerationException(e);
} catch (StateException e) {
throw new ContentUrlGenerationException(e);
} catch (ContentMappingException e) {
throw new ContentUrlGenerationException(e);
} catch (WCMException e) {
throw new ContentUrlGenerationException(e);
}
} else {
// no content path was given
// let the content URL generation chain handle the request
chain.writeURL(request, response);
}
if (isLogging) {
LOGGER.exiting(getClass().getName(), "writeURL");
}
}
/**
* Lookup the best matching target web content page for the content
*
* @param portletRequest
* The current portlet request
* @param portletResponse
* The current portlet request
* @param contentPath
* The path of the content
* @return The {@link ObjectID} of page found or <code>null</code>
*
* @throws ContentMappingException
* If an error occurred loading a content mapping
* @throws ModelException
* If an exception occurred while accessing a model object
* @throws WCMException
* If an exception occurred while accessing the WCM repository
* @throws StateException
* If an error occurred working with the portal state objects
*/
protected ObjectID lookupTargetPage(final PortletRequest portletRequest, final PortletResponse portletResponse,
final String contentPath) throws ContentMappingException, ModelException, WCMException, StateException {
final boolean isLogging = LOGGER.isLoggable(Level.FINEST);
if (isLogging) {
LOGGER.entering(getClass().getName(), "lookupTargetPage", new Object[] { contentPath });
}
ObjectID result = null;
// get the ID of the published item addressed by the content path
final DocumentIdIterator documentsIt = this.workspace.findByPath(contentPath,
Workspace.WORKFLOWSTATUS_PUBLISHED);
if (documentsIt.hasNext()) {
// get the IDs of the content and all its parents
final LinkedList<String> resourceIds = new LinkedList<String>();
final DocumentId documentId = documentsIt.next();
resourceIds.push(documentId.getId());
// load the IDs of the parents of the item
DocumentId parentId = documentId;
do {
Document doc = this.workspace.getById(parentId);
parentId = null;
if (doc instanceof Content) {
parentId = ((Content) doc).getDirectParent();
} else if (doc instanceof ContentLink) {
parentId = ((ContentLink) doc).getParentId();
} else if (doc instanceof SiteFrameworkContainer) {
parentId = ((SiteFrameworkContainer) doc).getParent();
}
if (parentId != null) {
resourceIds.push(parentId.getId());
}
} while (parentId != null);
// add the library of the content to the beginning
resourceIds.push(documentId.getContainingLibrary().getId());
if (isLogging) {
LOGGER.logp(Level.FINEST, getClass().getName(), "lookupTargetPage",
"Lookup up best matching web content page for resources [{0}] using the following IDs [{1}]",
new Object[] { contentPath, resourceIds });
}
// lookup the best matching web content page
final ContentMappingLocator contentMappinglocator = this.contentMappingInfoHome.getContentMappingLocator();
final LongestPathMatch match = contentMappinglocator.getLongestPathMatch(resourceIds,
getCurrentPage(portletRequest, portletResponse), new ContentMappingFilter() {
public void filterEntitledMappings(List<? extends ContentMapping> mappings) {
// filter out pages we cannot locate e.g. the
// user doesn't have access to or if the page is
// disabled
final Locator<ContentNode> contentNodeLocator = FriendlyUrlGenerationFilter.this.contentModel
.getLocator();
final Iterator<? extends ContentMapping> mappingsIt = mappings.iterator();
while (mappingsIt.hasNext()) {
if (contentNodeLocator.findByID(mappingsIt.next().getResourceID()) == null) {
mappingsIt.remove();
}
}
}
});
// if at least one match was found take the suggest content
// mapping further candidates might be found
final ContentMapping contentMapping = match.getContentMapping();
if (contentMapping != null) {
result = contentMapping.getResourceID();
}
}
if (isLogging) {
LOGGER.exiting(getClass().getName(), "lookupTargetPage", result);
}
return result;
}
/**
* Get the {@link ObjectID} of the target page from a target page
* configuration.
*
* @param portletRequest
* The current portlet request
* @param portletResponse
* The current portlet request
* @param targetPageConfig
* The target page configuration
* @return The {@link ObjectID} of the target page
*
* @throws SerializationException
* If the a page ID given as a character string cannot be
* serialized to an {@link ObjectID}
* @throws ModelException
* If an exception occurred while accessing a model object
* @throws StateException
* if an error occurred working with the portal state objects
*/
protected ObjectID getTargetPage(final PortletRequest portletRequest, final PortletResponse portletResponse,
final TargetPageConfig targetPageConfig) throws SerializationException, ModelException, StateException {
final boolean isLogging = LOGGER.isLoggable(Level.FINEST);
if (isLogging) {
LOGGER.entering(getClass().getName(), "getTargetPage", new Object[] { targetPageConfig });
}
ObjectID result = null;
if (targetPageConfig != null) {
if (targetPageConfig.useCurrentPage()) {
result = getCurrentPage(portletRequest, portletResponse);
} else {
final String pagePath = targetPageConfig.getPagePath();
if (pagePath != null && !pagePath.isEmpty()) {
// try to lookup the page treating the path as a URL mapping
result = getPageByUrlMapping(portletRequest, portletResponse, pagePath);
if (result == null) {
// if no mapping was found, check if the path is a
// valid friendly URL
final List<ObjectID> pages = getPagesByFriendlyUrl(portletRequest, portletResponse, pagePath);
if (pages != null && !pages.isEmpty()) {
// if multiple pages are found for simplicity use
// the
// first page more advance URL generation filter
// could
// do a disambiguation here and e.g. let the user
// choose
// what page to use
result = pages.get(0);
}
}
} else {
result = getPageById(targetPageConfig.getPageId());
}
}
}
if (isLogging) {
LOGGER.exiting(getClass().getName(), "getTargetPage", result);
}
return result;
}
/**
* Get the {@link ObjectID} of the page with the given ID or unique name
*
* @param pageId
* The ID or unique name of the page
* @return The {@link ObjectID} of the page
*
* @throws SerializationException
* If the a page ID given as a character string cannot be
* serialized to an {@link ObjectID}
*/
protected ObjectID getPageById(final String pageId) throws SerializationException {
final boolean isLogging = LOGGER.isLoggable(Level.FINEST);
if (isLogging) {
LOGGER.entering(getClass().getName(), "getPageById", new Object[] { pageId });
}
ObjectID result = null;
if (pageId != null && !pageId.isEmpty()) {
// de-serialize the ID
result = this.identification.deserialize(pageId);
}
if (isLogging) {
LOGGER.exiting(getClass().getName(), "getPageById", result);
}
return result;
}
/**
* Get the {@link ObjectID} of the current page
*
* @param portletRequest
* The current portlet request
* @param portletResponse
* The current portlet request
*
* @return The {@link ObjectID} of the current page
*
* @throws StateException
* if an error occurred working with the portal state objects
*/
protected ObjectID getCurrentPage(final PortletRequest portletRequest, final PortletResponse portletResponse)
throws StateException {
final boolean isLogging = LOGGER.isLoggable(Level.FINEST);
if (isLogging) {
LOGGER.entering(getClass().getName(), "getCurrentPage");
}
if (currentPage == null) {
final SelectionAccessor selectionAcc = this.selectionFactory.getSelectionAccessor(this.stateManager
.getStateHolder());
try {
currentPage = selectionAcc.getSelection();
} finally {
selectionAcc.dispose();
}
}
if (isLogging) {
LOGGER.exiting(getClass().getName(), "getCurrentPage", currentPage);
}
return currentPage;
}
/**
* Get the list of {@link ObjectID} of all page that are addressed by the
* passed friendly name
*
* @param portletRequest
* The current portlet request
* @param portletResponse
* The current portlet request
* @param friendlyName
* The friendly name
* @return List of all pages that are addressed by the passed friendly name
*
* @throws ModelException
* If looking up the page from a friendly URL fails
* @throws StateException
* if the state could not be accessed
*/
protected List<ObjectID> getPagesByFriendlyUrl(final PortletRequest portletRequest,
final PortletResponse portletResponse, final String friendlyName) throws ModelException, StateException {
final boolean isLogging = LOGGER.isLoggable(Level.FINEST);
if (isLogging) {
LOGGER.entering(getClass().getName(), "getPagesByFriendlyUrl", new Object[] { friendlyName });
}
List<ObjectID> result = null;
if (friendlyName != null && !friendlyName.isEmpty()) {
final SelectionResult bean = new DefaultSelectionResult();
this.friendlySelectionService.resolve(bean, friendlyName);
// the resulting node list is already AC filtered as a
// result of using a performing navigation model.
final List<ObjectID> nodelist = bean.getNodes();
if (nodelist != null && !nodelist.isEmpty() && bean.getFriendlyPath() != null) {
result = nodelist;
}
}
if (isLogging) {
LOGGER.exiting(getClass().getName(), "getPagesByFriendlyUrl", result);
}
return result;
}
/**
* Get the {@link ObjectID} of the page addressed by the passed compound
* name of a url mapping or <code>null</code> if no corresponding URL
* mapping or page exists or if the current user does not have access to it.
*
* @param portletRequest
* The current portlet request
* @param portletResponse
* The current portlet request
* @param urlMapping
* The compound name of the url mapping
* @return {@link ObjectID} of the page or <code>null</code>
* @throws ModelException
* If an exception occurred while accessing the url mapping
* model
*/
protected ObjectID getPageByUrlMapping(final PortletRequest request, final PortletResponse response,
final String urlMapping) throws ModelException {
final boolean isLogging = LOGGER.isLoggable(Level.FINEST);
if (isLogging) {
LOGGER.entering(getClass().getName(), "getPageByUrlMapping", new Object[] { urlMapping });
}
ObjectID result = null;
if (urlMapping != null && !urlMapping.isEmpty()) {
final BestMatchResult searchResult;
// different to friendly names a URL mapping must not begin with a /
if (urlMapping.charAt(0) == PATH_SEPARATOR.charAt(0)) {
searchResult = this.urlMappingModel.getLocator().findBestMatch(urlMapping.substring(1));
} else {
searchResult = this.urlMappingModel.getLocator().findBestMatch(urlMapping);
}
if (searchResult != null) {
final Context mappingCtx = searchResult.getContext();
if (ObjectTypeConstants.PORTAL_URL.getType().equals(mappingCtx.getAssignedObjectType())) {
final PortalURL url = (PortalURL) mappingCtx.getAssignedObject();
result = url.getReferencedResourceID();
}
}
}
if (isLogging) {
LOGGER.exiting(getClass().getName(), "getPageByUrlMapping", result);
}
return result;
}
/**
* Returns the path for the given content path relative to the site area
* mapped to the target page.
*
* Returns <code>null</code> if there is no content mapping set for the
* target page that is appropriate for the targeted content item.
*
* @param contentPath
* The fully qualified path of the target content item. Must not
* be <code>null</code>.
* @param pageId
* The object ID of the target page. Must not be
* <code>null</code>.
* @return The relative path which is the remainder of the content path
* after cutting off the content mapping prefix.
* May return <code>null</code>.
* Returns an empty string if the given path points directly
* to the site area that is mapped to the target page.
* @throws ContentMappingException
* If an exception occurred during lookup of the content mapping
* @throws WCMException
* If an exception occurred while accessing the WCM repository
* @throws UnsupportedEncodingException
* A requested character encoding is not supported
*/
protected String getRelativePathInfo(final String contentPath, final ObjectID pageId)
throws ContentMappingException, WCMException, UnsupportedEncodingException {
final boolean isLogging = LOGGER.isLoggable(Level.FINEST);
if (isLogging) {
LOGGER.entering(getClass().getName(), "getRelativePathInfo", new Object[] { contentPath, pageId });
}
String result = null;
final ContentMapping contentMapping = getDefaultContentMapping(pageId);
if (contentMapping != null) {
// lookup the path of the site area mapped to the page
String pathMapping = contentMapping.getContentPath();
if (pathMapping == null || pathMapping.isEmpty()) {
// lets lookup the path from the id
final String mappedId = contentMapping.getContentID();
if (mappedId != null && !mappedId.isEmpty()) {
pathMapping = this.workspace.getPathById(this.workspace.createDocumentId(mappedId), false, true);
}
}
if (isLogging) {
LOGGER.logp(Level.FINEST, getClass().getName(), "getRelativePathInfo",
"Page with ID [{0}] is mapped to [{1}]", new Object[] { pageId, pathMapping });
}
// calculate relative path = contentPath - mappingPath
if (pathMapping != null && !pathMapping.isEmpty()) {
// check if the content path is a children of the mapped path
// to do this split the path into its segments
if (pathMapping.charAt(0) == PATH_SEPARATOR.charAt(0)) {
pathMapping = pathMapping.substring(1);
}
final String[] partsPathMapping = PATH_SEPARATOR_PATTERN.split(pathMapping);
// also split path of content
String pathContent = contentPath;
if (pathContent.charAt(0) == PATH_SEPARATOR.charAt(0)) {
pathContent = pathContent.substring(1);
}
final String[] partsPathContent = PATH_SEPARATOR_PATTERN.split(pathContent);
// check if the content is a children of the mapped path
if (partsPathMapping.length <= partsPathContent.length) {
boolean isDescendant = true;
for (int i = 0; i < partsPathMapping.length && isDescendant; i++) {
if (!partsPathMapping[i].equalsIgnoreCase(partsPathContent[i])) {
isDescendant = false;
}
}
if (isDescendant) {
// determine how many descendant levels are between the
// content and the mapped site area
final int descendantLevels = partsPathContent.length - partsPathMapping.length;
if (descendantLevels > 0) {
// build children path which is
// everything after the parent
final StringBuilder tmp = new StringBuilder();
for (int i = 0; i < descendantLevels; i++) {
tmp.append(PATH_SEPARATOR);
tmp.append(URLEncoder.encode(partsPathContent[partsPathMapping.length + i], "UTF-8"));
}
result = tmp.toString();
} else if (descendantLevels == 0) {
// the content path points directly to the
// site area that is mapped to the page
result = "";
}
}
}
}
}
if (isLogging) {
LOGGER.exiting(getClass().getName(), "getRelativePathInfo", result);
}
return result;
}
/**
* Get the default content mapping of a page or <code>null</code> if no such
* mapping exists
*
* @param pageId
* The {@link ObjectID} of the page
* @return The default mapping of the page or <code>null</code> if no
* default mapping could be determined.
*
* @throws ContentMappingDataBackendException
* If an exception occurred during lookup of the content mapping
*/
protected ContentMapping getDefaultContentMapping(final ObjectID pageId) throws ContentMappingDataBackendException {
final boolean isLogging = LOGGER.isLoggable(Level.FINEST);
if (isLogging) {
LOGGER.entering(getClass().getName(), "getDefaultContentMapping", new Object[] { pageId });
}
// get the page default content mapping as friendly url path info is
// only set for default or system content mapping
final ContentMappingInfo contentMappingInfo = this.contentMappingInfoHome.getContentMappingInfo(pageId);
ContentMapping result = contentMappingInfo.getDefaultContentMapping();
if(result == null) {
// use system mapping as default
result = contentMappingInfo.getSystemContentMapping();
}
if (isLogging) {
LOGGER.exiting(getClass().getName(), "getDefaultContentMapping", result);
}
return result;
}
}