/**
* 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();
}
}