Example 2: Filter class

This sample filter generates the friendly URL.

The filter runs several steps to create the friendly URL:
  1. 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.
  2. 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.
  3. 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;
	}
}