/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use * this file except in compliance with the License. You may obtain a copy of the License at the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package org.apereo.portal.url; import com.google.common.base.Function; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.servlet.http.HttpServletRequest; import javax.xml.xpath.XPathExpression; import org.apache.commons.lang.StringUtils; import org.apereo.portal.IUserPreferencesManager; import org.apereo.portal.PortalException; import org.apereo.portal.concurrency.caching.RequestCache; import org.apereo.portal.dao.usertype.FunctionalNameType; import org.apereo.portal.layout.IStylesheetUserPreferencesService; import org.apereo.portal.layout.IStylesheetUserPreferencesService.PreferencesScope; import org.apereo.portal.layout.IUserLayout; import org.apereo.portal.layout.IUserLayoutManager; import org.apereo.portal.layout.PortletTabIdResolver; import org.apereo.portal.layout.node.IUserLayoutNodeDescription; import org.apereo.portal.layout.om.IStylesheetDescriptor; import org.apereo.portal.layout.om.IStylesheetParameterDescriptor; import org.apereo.portal.portlet.om.IPortletDefinition; import org.apereo.portal.portlet.om.IPortletEntity; import org.apereo.portal.portlet.om.IPortletEntityId; import org.apereo.portal.portlet.om.IPortletWindow; import org.apereo.portal.portlet.om.IPortletWindowId; import org.apereo.portal.portlet.registry.IPortletEntityRegistry; import org.apereo.portal.portlet.registry.IPortletWindowRegistry; import org.apereo.portal.user.IUserInstance; import org.apereo.portal.user.IUserInstanceManager; import org.apereo.portal.xml.xpath.XPathOperations; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * Maps tabs and portlets to folder names and back. Handles a single set of tabs and uses tab IDs * for folder names. * */ @Service public class SingleTabUrlNodeSyntaxHelper implements IUrlNodeSyntaxHelper { public static final String EXTERNAL_ID_ATTR = "externalId"; private static final char PORTLET_PATH_ELEMENT_SEPERATOR = '.'; protected final Logger logger = LoggerFactory.getLogger(getClass()); private String defaultLayoutNodeIdExpression = "/layout/folder/folder[@type='regular' and @hidden!='true'][$defaultTab]/@ID"; // private String tabIdExpression = "/layout/folder/folder[@ID=$nodeId or descendant::node()[@ID=$nodeId]]/@ID"; private String defaultTabParameter = "defaultTab"; private IUserInstanceManager userInstanceManager; private XPathOperations xpathOperations; private IStylesheetUserPreferencesService stylesheetUserPreferencesService; private IPortletWindowRegistry portletWindowRegistry; private IPortletEntityRegistry portletEntityRegistry; @Autowired public void setPortletWindowRegistry(IPortletWindowRegistry portletWindowRegistry) { this.portletWindowRegistry = portletWindowRegistry; } @Autowired public void setPortletEntityRegistry(IPortletEntityRegistry portletEntityRegistry) { this.portletEntityRegistry = portletEntityRegistry; } @Autowired public void setUserInstanceManager(IUserInstanceManager userInstanceManager) { this.userInstanceManager = userInstanceManager; } @Autowired public void setXpathOperations(XPathOperations xpathOperations) { this.xpathOperations = xpathOperations; } @Autowired public void setStylesheetUserPreferencesService( IStylesheetUserPreferencesService stylesheetUserPreferencesService) { this.stylesheetUserPreferencesService = stylesheetUserPreferencesService; } @Override public String getName() { return this.getClass().getSimpleName(); } @Override public String getDefaultLayoutNodeId(HttpServletRequest httpServletRequest) { final IUserInstance userInstance = this.userInstanceManager.getUserInstance(httpServletRequest); final IUserPreferencesManager preferencesManager = userInstance.getPreferencesManager(); final IUserLayoutManager userLayoutManager = preferencesManager.getUserLayoutManager(); final IUserLayout userLayout = userLayoutManager.getUserLayout(); //This logic is specific to tab/column layouts final String defaultTabIndex = this.getDefaultTabIndex(httpServletRequest); if (defaultTabIndex != null) { final String defaultTabId = this.getTabId(userLayout, defaultTabIndex); if (StringUtils.isNotEmpty(defaultTabId)) { return defaultTabId; } } this.logger.warn( "Failed to find default tab id for '" + userInstance.getPerson().getUserName() + "' with default tab index " + defaultTabIndex + ". Index 1 will be tried as a fall-back."); final String firstTabId = getTabId(userLayout, "1"); if (StringUtils.isNotEmpty(firstTabId)) { return firstTabId; } this.logger.warn( "Failed to find default tab id for '" + userInstance.getPerson().getUserName() + "' with default tab index 1. The user has no tabs."); return userLayout.getRootId(); } protected String getTabId(final IUserLayout userLayout, final String tabIndex) { return this.xpathOperations.doWithExpression( defaultLayoutNodeIdExpression, Collections.singletonMap("defaultTab", tabIndex), new Function<XPathExpression, String>() { @Override public String apply(XPathExpression xPathExpression) { return userLayout.findNodeId(xPathExpression); } }); } /** Get the index of the default tab for the user */ protected String getDefaultTabIndex(HttpServletRequest httpServletRequest) { final String stylesheetParameter = this.stylesheetUserPreferencesService.getStylesheetParameter( httpServletRequest, PreferencesScope.STRUCTURE, this.defaultTabParameter); if (stylesheetParameter != null) { return stylesheetParameter; } final IStylesheetDescriptor stylesheetDescriptor = this.stylesheetUserPreferencesService.getStylesheetDescriptor( httpServletRequest, PreferencesScope.STRUCTURE); final IStylesheetParameterDescriptor stylesheetParameterDescriptor = stylesheetDescriptor.getStylesheetParameterDescriptor(this.defaultTabParameter); if (stylesheetParameterDescriptor != null) { return stylesheetParameterDescriptor.getDefaultValue(); } return null; } @RequestCache(keyMask = {false, true}) @Override public List<String> getFolderNamesForLayoutNode( HttpServletRequest request, String layoutNodeId) { /* * Implementation note: * While the API allows one or more folder names, this implementation will only ever return * a List with zero or one element. It's not entirely clear that a List with zero members * was allowed by the interface definition, but this implementation will return an empty * list if the layoutNodeId cannot be found in the layout. */ final IUserInstance userInstance = this.userInstanceManager.getUserInstance(request); final IUserPreferencesManager preferencesManager = userInstance.getPreferencesManager(); final IUserLayoutManager userLayoutManager = preferencesManager.getUserLayoutManager(); final IUserLayout userLayout = userLayoutManager.getUserLayout(); final String tabId = userLayout.findNodeId(new PortletTabIdResolver(layoutNodeId)); if (StringUtils.isEmpty(tabId)) { return Collections.emptyList(); } String externalId = stylesheetUserPreferencesService.getLayoutAttribute( request, PreferencesScope.STRUCTURE, tabId, EXTERNAL_ID_ATTR); if (externalId != null) { final Map<String, String> allNodesAndValuesForAttribute = stylesheetUserPreferencesService.getAllNodesAndValuesForAttribute( request, PreferencesScope.STRUCTURE, EXTERNAL_ID_ATTR); boolean appendNodeId = false; for (final Entry<String, String> nodeAttributeEntry : allNodesAndValuesForAttribute.entrySet()) { final String entryNodeId = nodeAttributeEntry.getKey(); final String entryValue = nodeAttributeEntry.getValue(); if (!tabId.equals(entryNodeId) && externalId.equals(entryValue)) { appendNodeId = true; break; } } if (!FunctionalNameType.isValid(externalId)) { logger.warn( "ExternalId {} for tab {} is not a valid fname. It will be converted for use in the URL but this results in additional overhead", externalId, tabId); externalId = FunctionalNameType.makeValid(externalId); } if (appendNodeId) { externalId = externalId + PORTLET_PATH_ELEMENT_SEPERATOR + layoutNodeId; } logger.trace( "Tab identified by {} resolved to externalId {} " + "so returning that externalId as the sole folder name for node.", layoutNodeId, externalId); return Arrays.asList(externalId); } else { logger.trace( "Tab identified by {} had no externalId " + "so returning just {} as the sole folder name for node {}.", layoutNodeId, tabId, layoutNodeId); return Arrays.asList(tabId); } } @Override public String getLayoutNodeForFolderNames( HttpServletRequest request, List<String> folderNames) { /* * Implementation note: while the API specifies a List of folderNames, this implementation * only ever considers the first (presumably, only) value in the list. */ if (folderNames == null || folderNames.isEmpty()) { logger.warn( "Asked to get layout node for an empty or null folderNames ({}).", folderNames); return null; } //Check if the folder name is compound and parse it if it is String folderName = folderNames.get(0); String layoutNodeId = null; final int seperatorIndex = folderName.indexOf(PORTLET_PATH_ELEMENT_SEPERATOR); if (seperatorIndex > 0 && seperatorIndex < folderName.length() - 1) { layoutNodeId = folderName.substring(seperatorIndex + 1); folderName = folderName.substring(0, seperatorIndex); } if (folderNames.size() > 1) { logger.warn( "Asked to consider multiple folder names {}, " + "but ignoring all but the first which has been parsed as {}.", folderNames, folderName); } // Search the users layout attributes for a layout node with a matching externalId value String firstMatchingNodeId = null; final Map<String, String> allNodesAndValuesForAttribute = stylesheetUserPreferencesService.getAllNodesAndValuesForAttribute( request, PreferencesScope.STRUCTURE, EXTERNAL_ID_ATTR); for (final Entry<String, String> entry : allNodesAndValuesForAttribute.entrySet()) { final String value = entry.getValue(); //Have to test against the fname safe version of the externalId since the folderName could have already been translated if (value.equals(folderName) || FunctionalNameType.makeValid(value).equals(folderName)) { final String nodeId = entry.getKey(); if (nodeId.equals(layoutNodeId)) { //ExternalId matched as well as the layoutNodeId, clear the firstMatchingNodeId since we found the nodeId here logger.trace("Parsed folder names {} to nodeId {}.", folderNames, nodeId); return nodeId; } else if (firstMatchingNodeId == null) { firstMatchingNodeId = nodeId; } } } //If an explicit nodeId match isn't found but at least one matching externalId was found use that match if (firstMatchingNodeId != null) { layoutNodeId = firstMatchingNodeId; } //In this case the folderName must not have been an externalId, assume it is a layout node else if (layoutNodeId == null) { layoutNodeId = folderName; } //Verify the parsed layoutNodeId matches a node in the user's layout final IUserInstance userInstance = this.userInstanceManager.getUserInstance(request); final IUserPreferencesManager preferencesManager = userInstance.getPreferencesManager(); final IUserLayoutManager userLayoutManager = preferencesManager.getUserLayoutManager(); final IUserLayoutNodeDescription node; try { node = userLayoutManager.getNode(layoutNodeId); } catch (PortalException e) { logger.warn( "Parsed requested folder names {} to layoutNodeId {} " + "but did not match a node in the user layout.", folderNames, layoutNodeId, e); return null; } if (node == null) { logger.warn( "Parsed requested folder names to layoutNodeId {} " + "but did not match a node in the user layout.", folderNames, layoutNodeId); return null; } String nodeId = node.getId(); logger.trace("Resolved node id {} for folder names {}.", nodeId, folderNames); return nodeId; } @RequestCache(keyMask = {false, true}) @Override public String getFolderNameForPortlet( HttpServletRequest request, IPortletWindowId portletWindowId) { final IPortletWindow portletWindow = this.portletWindowRegistry.getPortletWindow(request, portletWindowId); final IPortletEntity portletEntity = portletWindow.getPortletEntity(); final IPortletDefinition portletDefinition = portletEntity.getPortletDefinition(); final String fname = portletDefinition.getFName(); final String layoutNodeId = portletEntity.getLayoutNodeId(); //Build the targeted portlet string (fname + subscribeId) return fname + PORTLET_PATH_ELEMENT_SEPERATOR + layoutNodeId; } @Override public IPortletWindowId getPortletForFolderName( HttpServletRequest request, String targetedLayoutNodeId, String folderName) { //Basic parsing of the final String fname; String subscribeId = null; final int seperatorIndex = folderName.indexOf(PORTLET_PATH_ELEMENT_SEPERATOR); if (seperatorIndex <= 0 || seperatorIndex + 1 == folderName.length()) { fname = folderName; } else { fname = folderName.substring(0, seperatorIndex); subscribeId = folderName.substring(seperatorIndex + 1); } //If a subscribeId was provided validate that it matches up with the fname if (subscribeId != null) { final IUserInstance userInstance = this.userInstanceManager.getUserInstance(request); final IPortletEntity portletEntity = this.portletEntityRegistry.getOrCreatePortletEntity( request, userInstance, subscribeId); if (portletEntity == null || !fname.equals(portletEntity.getPortletDefinition().getFName())) { //If no entity found or the fname doesn't match ignore the provided subscribeId by setting it to null subscribeId = null; } else { //subscribeId matches fname, lookup the window for the entity and return the windowId final IPortletEntityId portletEntityId = portletEntity.getPortletEntityId(); final IPortletWindow portletWindow = this.portletWindowRegistry.getOrCreateDefaultPortletWindow( request, portletEntityId); if (portletWindow == null) { return null; } return portletWindow.getPortletWindowId(); } } //No valid subscribeId, find the best match based on the fname //If a layout node is targeted then look for a matching subscribeId under that targeted node if (targetedLayoutNodeId != null) { final IUserInstance userInstance = this.userInstanceManager.getUserInstance(request); final IUserPreferencesManager preferencesManager = userInstance.getPreferencesManager(); final IUserLayoutManager userLayoutManager = preferencesManager.getUserLayoutManager(); //First look for the layout node only under the specified folder subscribeId = userLayoutManager.getSubscribeId(targetedLayoutNodeId, fname); } //Find a subscribeId based on the fname final IPortletWindow portletWindow; if (subscribeId == null) { portletWindow = this.portletWindowRegistry.getOrCreateDefaultPortletWindowByFname( request, fname); } else { portletWindow = this.portletWindowRegistry.getOrCreateDefaultPortletWindowByLayoutNodeId( request, subscribeId); } if (portletWindow == null) { return null; } return portletWindow.getPortletWindowId(); } }