Example 2: Filter class
Create a content URL generation filter classThis sample filter generates the friendly URL.
The filter performs 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 specified by the UrlCmpnt tag.
- If a target page is identified, the filter verifies the page is a web content page with a content association. The filter then validates 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 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 associated with the target page.
/****************************************************************** * Copyright IBM Corp. 2011 ******************************************************************/ 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 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> 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 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 && !relativePathInfo.isEmpty()) { // 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); 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 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>. * * @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(); } } } } } 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; } }