/**
* 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.layout.dlm;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.Vector;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.xpath.XPathConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apereo.portal.IUserIdentityStore;
import org.apereo.portal.IUserProfile;
import org.apereo.portal.PortalException;
import org.apereo.portal.events.IPortalLayoutEventFactory;
import org.apereo.portal.layout.IUserLayout;
import org.apereo.portal.layout.IUserLayoutManager;
import org.apereo.portal.layout.IUserLayoutStore;
import org.apereo.portal.layout.PortletSubscribeIdResolver;
import org.apereo.portal.layout.node.IUserLayoutChannelDescription;
import org.apereo.portal.layout.node.IUserLayoutFolderDescription;
import org.apereo.portal.layout.node.IUserLayoutNodeDescription;
import org.apereo.portal.layout.node.IUserLayoutNodeDescription.LayoutNodeType;
import org.apereo.portal.layout.node.UserLayoutFolderDescription;
import org.apereo.portal.layout.simple.SimpleLayout;
import org.apereo.portal.portlet.om.IPortletDefinition;
import org.apereo.portal.portlet.om.IPortletDefinitionParameter;
import org.apereo.portal.portlet.registry.IPortletDefinitionRegistry;
import org.apereo.portal.security.AdminEvaluator;
import org.apereo.portal.security.IAuthorizationPrincipal;
import org.apereo.portal.security.IAuthorizationService;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.security.PersonFactory;
import org.apereo.portal.spring.locator.PortletDefinitionRegistryLocator;
import org.apereo.portal.xml.XmlUtilities;
import org.apereo.portal.xml.xpath.XPathOperations;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* A layout manager that provides layout control through layout fragments that are derived from
* regular portal user accounts.
*
* @since 2.5
*/
public class DistributedLayoutManager implements IUserLayoutManager, InitializingBean {
private static final Log LOG = LogFactory.getLog(DistributedLayoutManager.class);
private XmlUtilities xmlUtilities;
private ILayoutCachingService layoutCachingService;
private IUserLayoutStore distributedLayoutStore;
private XPathOperations xpathOperations;
private IPortalLayoutEventFactory portalEventFactory;
private IAuthorizationService authorizationService;
private IUserIdentityStore userIdentityStore;
protected final IPerson owner;
protected final IUserProfile profile;
protected static final Random rnd = new Random();
protected String cacheKey = null; // Must be "updated" prior to use
protected String rootNodeId = null;
private boolean channelsAdded = false;
private boolean isFragmentOwner = false;
public DistributedLayoutManager(IPerson owner, IUserProfile profile) throws PortalException {
if (owner == null) {
throw new PortalException(
"Unable to instantiate DistributedLayoutManager. "
+ "A non-null owner must to be specified.");
}
if (profile == null) {
throw new PortalException(
"Unable to instantiate DistributedLayoutManager for "
+ owner.getAttribute(IPerson.USERNAME)
+ ". A "
+ "non-null profile must to be specified.");
}
// cache the relatively lightweight userprofile for use in layout PLF loading
owner.setAttribute(IUserProfile.USER_PROFILE, profile);
this.owner = owner;
this.profile = profile;
// Must always initialize the cacheKey to a generated value
this.updateCacheKey();
}
@Autowired
public void setAuthorizationService(IAuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}
@Autowired
public void setUserIdentityStore(IUserIdentityStore userIdentityStore) {
this.userIdentityStore = userIdentityStore;
}
@Autowired
public void setXpathOperations(XPathOperations xpathOperations) {
this.xpathOperations = xpathOperations;
}
@Autowired
public void setXmlUtilities(XmlUtilities xmlUtilities) {
this.xmlUtilities = xmlUtilities;
}
@Autowired
public void setLayoutCachingService(ILayoutCachingService layoutCachingService) {
this.layoutCachingService = layoutCachingService;
}
@Autowired
public void setDistributedLayoutStore(IUserLayoutStore distributedLayoutStore) {
this.distributedLayoutStore = distributedLayoutStore;
}
@Autowired
public void setPortalEventFactory(IPortalLayoutEventFactory portalEventFactory) {
this.portalEventFactory = portalEventFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
// Ensure a new layout gets loaded whenever a user logs in except for guest users
if (!owner.isGuest()) {
this.layoutCachingService.removeCachedLayout(owner, profile);
}
this.loadUserLayout();
// verify that we have the minimum layout necessary to render the
// portal and reset it if we do not.
this.getRootFolderId();
// This listener determines if one or more channels have been
// added, and sets a state variable which is reset when the
// layout saved event is triggered.
// this.addLayoutEventListener(new LayoutEventListenerAdapter()
// {
// @Override
// public void channelAdded(LayoutEvent ev) {
// channelsAdded = true;
// }
// @Override
// public void layoutSaved() {
// channelsAdded = false;
// }
// });
}
private void setUserLayoutDOM(DistributedUserLayout userLayout) {
this.layoutCachingService.cacheLayout(owner, profile, userLayout);
this.updateCacheKey();
// determine if this is a layout fragment by looking at the root node
// for a cp:fragment attribute.
Element layout = userLayout.getLayout().getDocumentElement();
Node attr = layout.getAttributeNodeNS(Constants.NS_URI, Constants.LCL_FRAGMENT_NAME);
this.isFragmentOwner = attr != null;
}
@SuppressWarnings("deprecation")
@Deprecated
@Override
public Document getUserLayoutDOM() {
final DistributedUserLayout userLayout = getDistributedUserLayout();
return userLayout.getLayout();
}
protected DistributedUserLayout getDistributedUserLayout() {
DistributedUserLayout userLayout =
this.layoutCachingService.getCachedLayout(owner, profile);
if (null == userLayout) {
if (LOG.isDebugEnabled()) {
LOG.debug("Load from store for " + owner.getAttribute(IPerson.USERNAME));
}
userLayout = this.distributedLayoutStore.getUserLayout(this.owner, this.profile);
final Document userLayoutDocument = userLayout.getLayout();
// DistributedLayoutManager shall gracefully remove channels
// that the user isn't authorized to render from folders of type
// 'header' and 'footer'.
IAuthorizationPrincipal principal =
authorizationService.newPrincipal(owner.getUserName(), IPerson.class);
NodeList nodes = userLayoutDocument.getElementsByTagName("folder");
for (int i = 0; i < nodes.getLength(); i++) {
Element fd = (Element) nodes.item(i);
String type = fd.getAttribute("type");
if (type != null
&& (type.equals("header")
|| type.equals("footer")
|| type.equals("sidebar"))) {
// Here's where we do the work...
if (LOG.isDebugEnabled()) {
LOG.debug(
"RDBMUserLayoutStore examining the '"
+ type
+ "' folder of user '"
+ owner.getUserName()
+ "' for non-authorized channels.");
}
NodeList channels = fd.getElementsByTagName("channel");
for (int j = 0; j < channels.getLength(); j++) {
Element ch = (Element) channels.item(j);
try {
String chanId = ch.getAttribute("chanID");
if (!principal.canRender(chanId)) {
fd.removeChild(ch);
if (LOG.isDebugEnabled()) {
LOG.debug(
"RDBMUserLayoutStore removing channel '"
+ ch.getAttribute("fname")
+ "' from the header or footer of user '"
+ owner.getUserName()
+ "' because he/she isn't authorized to render it.");
}
}
} catch (Throwable t) {
// Log this...
LOG.warn(
"RDBMUserLayoutStore was unable to analyze channel element with Id="
+ ch.getAttribute("chanID"),
t);
}
}
}
}
setUserLayoutDOM(userLayout);
}
return userLayout;
}
@Override
public XMLEventReader getUserLayoutReader() {
Document ul = this.getUserLayoutDOM();
if (ul == null) {
throw new PortalException(
"User layout has not been initialized for "
+ owner.getAttribute(IPerson.USERNAME));
}
final XMLInputFactory xmlInputFactory = this.xmlUtilities.getXmlInputFactory();
final DOMSource layoutSoure = new DOMSource(ul);
try {
return xmlInputFactory.createXMLEventReader(layoutSoure);
} catch (XMLStreamException e) {
throw new RuntimeException(
"Failed to create Layout XMLStreamReader for user: "
+ owner.getAttribute(IPerson.USERNAME),
e);
}
}
public synchronized void loadUserLayout() throws PortalException {
this.loadUserLayout(false);
}
public synchronized void loadUserLayout(boolean reload) throws PortalException {
Document uli = null;
try {
//Clear the loaded document first if this is a forced reload
if (reload) {
this.layoutCachingService.removeCachedLayout(owner, profile);
}
uli = getUserLayoutDOM();
} catch (Exception e) {
throw new PortalException(
"Exception encountered while "
+ "reading a layout for userId="
+ this.owner.getID()
+ ", profileId="
+ this.profile.getProfileId(),
e);
}
if (uli == null) {
throw new PortalException(
"Null user layout returned "
+ "for ownerId=\""
+ owner.getID()
+ "\", profileId=\""
+ profile.getProfileId()
+ "\", layoutId=\""
+ profile.getLayoutId()
+ "\"");
}
}
public synchronized void saveUserLayout() throws PortalException {
Document uld = this.getUserLayoutDOM();
if (uld == null) {
throw new PortalException(
"UserLayout has not been initialized for "
+ owner.getAttribute(IPerson.USERNAME)
+ ".");
}
try {
this.distributedLayoutStore.setUserLayout(this.owner, this.profile, uld, channelsAdded);
} catch (Exception e) {
throw new PortalException(
"Exception encountered while "
+ "saving layout for userId="
+ this.owner.getID()
+ ", profileId="
+ this.profile.getProfileId(),
e);
}
this.channelsAdded = false;
}
@Override
public Set<String> getAllSubscribedChannels() {
final Document uld = this.getUserLayoutDOM();
if (uld == null) {
throw new PortalException(
"UserLayout has not been initialized for "
+ owner.getAttribute(IPerson.USERNAME));
}
final NodeList channelElements = uld.getElementsByTagName(CHANNEL);
final Set<String> allSubscribedChannels =
new LinkedHashSet<String>(channelElements.getLength());
for (int nodeIndex = 0; nodeIndex < channelElements.getLength(); nodeIndex++) {
final Element channelElement = (Element) channelElements.item(nodeIndex);
final String subscribeId = channelElement.getAttribute("ID");
allSubscribedChannels.add(subscribeId);
}
return allSubscribedChannels;
}
public IUserLayoutNodeDescription getNode(String nodeId) throws PortalException {
if (nodeId == null) return null;
Document uld = this.getUserLayoutDOM();
if (uld == null)
throw new PortalException(
"UserLayout has not been initialized for "
+ owner.getAttribute(IPerson.USERNAME)
+ ".");
// find an element with a given id
Element element = uld.getElementById(nodeId);
if (element == null) {
throw new PortalException(
"Element with ID=\""
+ nodeId
+ "\" doesn't exist for "
+ owner.getAttribute(IPerson.USERNAME)
+ ".");
}
// instantiate the node description
final IUserLayoutNodeDescription desc = createNodeDescription(element);
return desc;
}
public IUserLayoutNodeDescription addNode(
IUserLayoutNodeDescription node, String parentId, String nextSiblingId)
throws PortalException {
boolean isChannel = false;
IUserLayoutNodeDescription parent = this.getNode(parentId);
if (canAddNode(node, parent, nextSiblingId)) {
// assign new Id
try {
if (node instanceof IUserLayoutChannelDescription) {
isChannel = true;
node.setId(this.distributedLayoutStore.generateNewChannelSubscribeId(owner));
} else {
node.setId(this.distributedLayoutStore.generateNewFolderId(owner));
}
} catch (Exception e) {
throw new PortalException(
"Exception encountered while "
+ "generating new user layout node Id for for "
+ owner.getAttribute(IPerson.USERNAME),
e);
}
Document uld = getUserLayoutDOM();
Element childElement = node.getXML(uld);
Element parentElement = uld.getElementById(parentId);
if (nextSiblingId == null) {
parentElement.appendChild(childElement);
} else {
Node nextSibling = uld.getElementById(nextSiblingId);
parentElement.insertBefore(childElement, nextSibling);
}
// register element id
childElement.setIdAttribute(Constants.ATT_ID, true);
childElement.setAttribute(Constants.ATT_ID, node.getId());
this.updateCacheKey();
// push into the user's real layout that gets persisted.
HandlerUtils.createPlfNodeAndPath(childElement, isChannel, owner);
// fire event
final int layoutId = this.getLayoutId();
if (isChannel) {
this.channelsAdded = true;
final String fname = ((IUserLayoutChannelDescription) node).getFunctionalName();
this.portalEventFactory.publishPortletAddedToLayoutPortalEvent(
this, this.owner, layoutId, parent.getId(), fname);
} else {
this.portalEventFactory.publishFolderAddedToLayoutPortalEvent(
this, this.owner, layoutId, node.getId());
}
return node;
}
return null;
}
public boolean moveNode(String nodeId, String parentId, String nextSiblingId)
throws PortalException {
IUserLayoutNodeDescription parent = this.getNode(parentId);
IUserLayoutNodeDescription node = this.getNode(nodeId);
String oldParentNodeId = getParentId(nodeId);
if (canMoveNode(node, parent, nextSiblingId)) {
// must be a folder
Document uld = this.getUserLayoutDOM();
Element childElement = uld.getElementById(nodeId);
Element parentElement = uld.getElementById(parentId);
if (nextSiblingId == null) {
parentElement.appendChild(childElement);
} else {
Node nextSibling = uld.getElementById(nextSiblingId);
parentElement.insertBefore(childElement, nextSibling);
}
this.updateCacheKey();
// propagate the change into the PLF
Element oldParent = uld.getElementById(oldParentNodeId);
TabColumnPrefsHandler.moveElement(childElement, oldParent, owner);
// fire event
final int layoutId = this.getLayoutId();
if (node instanceof IUserLayoutChannelDescription) {
this.channelsAdded = true;
final String fname = ((IUserLayoutChannelDescription) node).getFunctionalName();
this.portalEventFactory.publishPortletMovedInLayoutPortalEvent(
this, this.owner, layoutId, oldParentNodeId, parent.getId(), fname);
} else {
this.portalEventFactory.publishFolderMovedInLayoutPortalEvent(
this, this.owner, layoutId, oldParentNodeId, parent.getId());
}
return true;
}
return false;
}
public boolean deleteNode(String nodeId) throws PortalException {
if (canDeleteNode(nodeId)) {
IUserLayoutNodeDescription nodeDescription = this.getNode(nodeId);
String parentNodeId = this.getParentId(nodeId);
Document uld = this.getUserLayoutDOM();
Element ilfNode = uld.getElementById(nodeId);
Node parent = ilfNode.getParentNode();
if (parent != null) {
parent.removeChild(ilfNode);
} else {
throw new PortalException(
"Node \""
+ nodeId
+ "\" has a NULL parent for layout of "
+ owner.getAttribute(IPerson.USERNAME)
+ ".");
}
this.updateCacheKey();
// now push into the PLF
TabColumnPrefsHandler.deleteNode(ilfNode, owner);
// inform the listeners
final int layoutId = this.getLayoutId();
if (nodeDescription instanceof IUserLayoutChannelDescription) {
final IUserLayoutChannelDescription userLayoutChannelDescription =
(IUserLayoutChannelDescription) nodeDescription;
this.portalEventFactory.publishPortletDeletedFromLayoutPortalEvent(
this,
this.owner,
layoutId,
parentNodeId,
userLayoutChannelDescription.getFunctionalName());
} else {
this.portalEventFactory.publishFolderDeletedFromLayoutPortalEvent(
this,
this.owner,
layoutId,
parentNodeId,
nodeDescription.getId(),
nodeDescription.getName());
}
return true;
}
return false;
}
/**
* Handles pushing changes made to the passed-in node into the user's layout. If the node is an
* ILF node then the change is recorded via directives in the PLF if such changes are allowed by
* the owning fragment. If the node is a user owned node then the changes are applied directly
* to the corresponding node in the PLF.
*/
public synchronized boolean updateNode(IUserLayoutNodeDescription node) throws PortalException {
if (canUpdateNode(node)) {
String nodeId = node.getId();
IUserLayoutNodeDescription oldNode = getNode(nodeId);
if (oldNode instanceof IUserLayoutChannelDescription) {
IUserLayoutChannelDescription oldChanDesc = (IUserLayoutChannelDescription) oldNode;
if (!(node instanceof IUserLayoutChannelDescription)) {
throw new PortalException(
"Change channel to folder is "
+ "not allowed by updateNode() method! Occurred "
+ "in layout for "
+ owner.getAttribute(IPerson.USERNAME)
+ ".");
}
IUserLayoutChannelDescription newChanDesc = (IUserLayoutChannelDescription) node;
updateChannelNode(nodeId, newChanDesc, oldChanDesc);
} else {
// must be a folder
IUserLayoutFolderDescription oldFolderDesc = (IUserLayoutFolderDescription) oldNode;
if (oldFolderDesc.getId().equals(getRootFolderId()))
throw new PortalException("Update of root node is not currently allowed!");
if (node instanceof IUserLayoutFolderDescription) {
IUserLayoutFolderDescription newFolderDesc =
(IUserLayoutFolderDescription) node;
updateFolderNode(nodeId, newFolderDesc, oldFolderDesc);
}
}
this.updateCacheKey();
return true;
}
return false;
}
/**
* Compares the new folder description object with the old folder description object to
* determine what items were changed and if those changes are allowed. Once all changes are
* verified as being allowed changes then they are pushed into both the ILF and the PLF as
* appropriate. No changes are made until we determine that all changes are allowed.
*
* @param nodeId
* @param newFolderDesc
* @param oldFolderDesc
* @throws PortalException
*/
private void updateFolderNode(
String nodeId,
IUserLayoutFolderDescription newFolderDesc,
IUserLayoutFolderDescription oldFolderDesc)
throws PortalException {
Element ilfNode = (Element) getUserLayoutDOM().getElementById(nodeId);
List<ILayoutProcessingAction> pendingActions = new ArrayList<ILayoutProcessingAction>();
/*
* see what structure attributes changed if any and see if allowed.
*
* CHANNEL ATTRIBUTES that currently can be EDITED in DLM are:
* name - in both fragments and regular layouts
* dlm:moveAllowed - only on fragments
* dlm:editAllowed - only on fragments
* dlm:deleteAllowed - only on fragments
* dlm:addChildAllowed - only on fragments
*/
// ATT: DLM Restrictions
if (isFragmentOwner
&& (newFolderDesc.isDeleteAllowed() != oldFolderDesc.isDeleteAllowed()
|| newFolderDesc.isEditAllowed() != oldFolderDesc.isEditAllowed()
|| newFolderDesc.isAddChildAllowed() != oldFolderDesc.isAddChildAllowed()
|| newFolderDesc.isMoveAllowed() != oldFolderDesc.isMoveAllowed())) {
pendingActions.add(
new LPAEditRestriction(
owner,
ilfNode,
newFolderDesc.isMoveAllowed(),
newFolderDesc.isDeleteAllowed(),
newFolderDesc.isEditAllowed(),
newFolderDesc.isAddChildAllowed()));
}
// ATT: Name
updateNodeAttribute(
ilfNode,
nodeId,
Constants.ATT_NAME,
newFolderDesc.getName(),
oldFolderDesc.getName(),
pendingActions);
/*
* if we make it to this point then all edits made are allowed so
* process the actions to push the edits into the layout
*/
for (Iterator itr = pendingActions.iterator(); itr.hasNext(); ) {
ILayoutProcessingAction action = (ILayoutProcessingAction) itr.next();
action.perform();
}
}
/**
* Handles checking for updates to a named attribute, verifying such change is allowed, and
* generates an action object to make that change.
*
* @param ilfNode the node in the viewed layout
* @param nodeId the id of the ilfNode
* @param attName the attribute to be checked
* @param newVal the attribute's new value
* @param oldVal the attribute's old value
* @param pendingActions the set of actions for adding an action
* @throws PortalException if the change is not allowed
*/
private void updateNodeAttribute(
Element ilfNode,
String nodeId,
String attName,
String newVal,
String oldVal,
List<ILayoutProcessingAction> pendingActions)
throws PortalException {
if (newVal == null && oldVal != null
|| newVal != null && oldVal == null
|| (newVal != null && oldVal != null && !newVal.equals(oldVal))) {
boolean isIncorporated = nodeId.startsWith(Constants.FRAGMENT_ID_USER_PREFIX);
if (isIncorporated) {
/*
* Is a change to this attribute allowed?
*/
FragmentNodeInfo fragNodeInf =
this.distributedLayoutStore.getFragmentNodeInfo(nodeId);
if (fragNodeInf == null) {
/*
* null should only happen if a node was deleted in the
* fragment and a user happened to already be logged in and
* edited an attribute on that node.
*/
pendingActions.add(
new LPAChangeAttribute(
nodeId, attName, newVal, owner, ilfNode));
} else if (!fragNodeInf.canOverrideAttributes()) {
/*
* It isn't overrideable.
*/
throw new PortalException(
"Layout element '"
+ fragNodeInf.getAttributeValue(attName)
+ "' does not allow overriding attribute '"
+ attName
+ "'.");
} else if (!fragNodeInf.getAttributeValue(attName).equals(newVal)) {
/*
* If we get here we can override and the value is
* different than that in the fragment so make the change.
*/
pendingActions.add(
new LPAChangeAttribute(
nodeId, attName, newVal, owner, ilfNode));
} else {
/*
* The new value matches that in the fragment.
*/
pendingActions.add(
new LPAResetAttribute(
nodeId,
attName,
fragNodeInf.getAttributeValue(attName),
owner,
ilfNode));
}
} else {
/*
* Node owned by user so no checking needed. Just change it.
*/
pendingActions.add(
new LPAChangeAttribute(
nodeId, attName, newVal, owner, ilfNode));
}
}
}
/**
* Compares the new channel description object with the old channel description object to
* determine what items were changed and if those changes are allowed. Once all changes are
* verified as being allowed changes then they are pushed into both the ILF and the PLF as
* appropriate. No changes are made until we determine that all changes are allowed.
*
* @param nodeId
* @param newChanDesc
* @param oldChanDesc
* @throws PortalException
*/
private void updateChannelNode(
String nodeId,
IUserLayoutChannelDescription newChanDesc,
IUserLayoutChannelDescription oldChanDesc)
throws PortalException {
Element ilfNode = (Element) getUserLayoutDOM().getElementById(nodeId);
List<ILayoutProcessingAction> pendingActions = new ArrayList<ILayoutProcessingAction>();
boolean isIncorporated = nodeId.startsWith(Constants.FRAGMENT_ID_USER_PREFIX);
/*
* see what structure attributes changed if any and see if allowed.
*
* CHANNEL ATTRIBUTES that currently can be EDITED in DLM are:
* dlm:moveAllowed - only on fragments
* dlm:editAllowed - only on fragments
* dlm:deleteAllowed - only on fragments
*/
// ATT: DLM Restrictions
if (isFragmentOwner
&& (newChanDesc.isDeleteAllowed() != oldChanDesc.isDeleteAllowed()
|| newChanDesc.isEditAllowed() != oldChanDesc.isEditAllowed()
|| newChanDesc.isMoveAllowed() != oldChanDesc.isMoveAllowed())) {
pendingActions.add(
new LPAEditRestriction(
owner,
ilfNode,
newChanDesc.isMoveAllowed(),
newChanDesc.isDeleteAllowed(),
newChanDesc.isEditAllowed(),
newChanDesc.isAddChildAllowed()));
}
// ATT: other? if other attributes should be editable in DLM on channels
// we can add calls like this to enable such support.
// updateNodeAttribute(ilfNode, nodeId, "hidden",
// newChanDesc.getName(), oldChanDesc.getName(), pendingActions);
/*
* now we loop through all parameters in the new channel description and
* see if there is a corresponding parameter in the old channel
* description and see if the change is allowed. For each allowed change
* we add an object that will make such a change once all changes have
* been approved. As we find matches in the old channel description we
* remove those parameters. Then any left there after processing those
* of the new channel description indicate parameters that were removed.
*/
FragmentChannelInfo fragChanInf = null;
Map pubParms = getPublishedChannelParametersMap(newChanDesc.getChannelPublishId());
if (isIncorporated)
fragChanInf = this.distributedLayoutStore.getFragmentChannelInfo(nodeId);
Map oldParms = new HashMap(oldChanDesc.getParameterMap());
for (Iterator itr = newChanDesc.getParameterMap().entrySet().iterator(); itr.hasNext(); ) {
Map.Entry e = (Entry) itr.next();
String name = (String) e.getKey();
String newVal = (String) e.getValue();
String oldVal = (String) oldParms.remove(name);
if (oldVal == null) {
/*
* not in old description so this is a new ad-hoc parameter
*/
pendingActions.add(new LPAAddParameter(nodeId, name, newVal, owner, ilfNode));
} else if (!oldVal.equals(newVal)) {
if (isIncorporated) {
/*
* if the fragment does not have a value for this parm then
* this is an ad-hoc value and we need a directive to
* persist the user's desired value. if the frament does
* have a value and it is the same as the new value then we
* can remove the override since it won't accomplish
* anything. if the fragment does have a value and it is
* different then we need the directive to persist the
* user's desired value.
*/
String fragValue = fragChanInf.getParameterValue(name);
if (fragValue == null) {
/*
* so fragment doesn't override. See if the value
* specified matches that of the channel definition
*/
IPortletDefinitionParameter cp =
(IPortletDefinitionParameter) pubParms.get(name);
if (cp != null && cp.getValue().equals(newVal))
/*
* new value matches that of published channel to
* remove any user parameter spec since not needed
*/
pendingActions.add(
new LPARemoveParameter(nodeId, name, owner, ilfNode));
else
/*
* value doesn't match that of published chanel so
* we need change any existing parameter spec or add
* a new one if it doesn't exist.
*/
pendingActions.add(
new LPAChangeParameter(nodeId, name, newVal, owner, ilfNode));
} else if (!fragValue.equals(newVal)) {
/*
* so fragment does specify and user value is different
* so change any existing parameter spec or add a new
* one if it doesn't exist.
*/
pendingActions.add(
new LPAChangeParameter(nodeId, name, newVal, owner, ilfNode));
} else {
/*
* new val same as fragment value so don't persist.
* remove any parameter spec if it exists.
*/
pendingActions.add(
new LPAResetParameter(nodeId, name, fragValue, owner, ilfNode));
}
} else // not incorporated from a fragment
{
/*
* see if the value specified matches that of the channel
* definition.
*/
IPortletDefinitionParameter cp =
(IPortletDefinitionParameter) pubParms.get(name);
if (cp != null && cp.getValue().equals(newVal))
pendingActions.add(new LPARemoveParameter(nodeId, name, owner, ilfNode));
else
pendingActions.add(
new LPAChangeParameter(nodeId, name, newVal, owner, ilfNode));
}
}
}
/*
* So any parameters remaining in the oldParms map at this point didn't
* match those in the new channel description which means that they were
* removed. So remove any parameter spec if it exists.
*/
for (Iterator itr = oldParms.entrySet().iterator(); itr.hasNext(); ) {
Map.Entry e = (Entry) itr.next();
String name = (String) e.getKey();
pendingActions.add(new LPARemoveParameter(nodeId, name, owner, ilfNode));
}
/*
* if we make it to this point then all edits made are allowed so
* process the actions to push the edits into the layout
*/
for (Iterator itr = pendingActions.iterator(); itr.hasNext(); ) {
ILayoutProcessingAction action = (ILayoutProcessingAction) itr.next();
action.perform();
}
}
/**
* Return a map parameter names to channel parameter objects representing the parameters
* specified at publish time for the channel with the passed-in publish id.
*
* @param channelPublishId
* @return
* @throws PortalException
*/
private Map getPublishedChannelParametersMap(String channelPublishId) throws PortalException {
try {
IPortletDefinitionRegistry registry =
PortletDefinitionRegistryLocator.getPortletDefinitionRegistry();
IPortletDefinition def = registry.getPortletDefinition(channelPublishId);
return def.getParametersAsUnmodifiableMap();
} catch (Exception e) {
throw new PortalException("Unable to acquire channel definition.", e);
}
}
public boolean canAddNode(
IUserLayoutNodeDescription node, String parentId, String nextSiblingId)
throws PortalException {
return this.canAddNode(node, this.getNode(parentId), nextSiblingId);
}
protected boolean canAddNode(
IUserLayoutNodeDescription node,
IUserLayoutNodeDescription parent,
String nextSiblingId)
throws PortalException {
// make sure sibling exists and is a child of nodeId
if (nextSiblingId != null && !nextSiblingId.equals("")) {
IUserLayoutNodeDescription sibling = getNode(nextSiblingId);
if (sibling == null) {
throw new PortalException(
"Unable to find a sibling node "
+ "with id=\""
+ nextSiblingId
+ "\". Occurred "
+ "in layout for "
+ owner.getAttribute(IPerson.USERNAME)
+ ".");
}
if (!parent.getId().equals(getParentId(nextSiblingId))) {
throw new PortalException(
"Given sibling (\""
+ nextSiblingId
+ "\") is not a child of a given parentId (\""
+ parent.getId()
+ "\"). Occurred "
+ "in layout for "
+ owner.getAttribute(IPerson.USERNAME)
+ ".");
}
}
// todo if isFragmentOwner should probably verify both node and parent are part of the
// same layout fragment as the fragment owner to insure a misbehaving front-end doesn't
// do an improper operation.
if (parent == null || !(node.isMoveAllowed() || isFragmentOwner)) return false;
if (parent instanceof IUserLayoutFolderDescription
&& !(((IUserLayoutFolderDescription) parent).isAddChildAllowed())
&& !isFragmentOwner) return false;
if (nextSiblingId == null || nextSiblingId.equals("")) // end of list targeted
return true;
// so lets see if we can place it at the end of the sibling list and
// hop left until we get into the correct position.
Enumeration sibIds = getVisibleChildIds(parent.getId());
List sibs = Collections.list(sibIds);
if (sibs.size() == 0) // last node in list so should be ok
return true;
// reverse scan so that as changes are made the order of the, as yet,
// unprocessed nodes is not altered.
for (int idx = sibs.size() - 1; idx >= 0; idx--) {
IUserLayoutNodeDescription prev = getNode((String) sibs.get(idx));
if (!isFragmentOwner && !MovementRules.canHopLeft(node, prev)) return false;
if (prev.getId().equals(nextSiblingId)) return true;
}
return false; // oops never found the sib
}
public boolean canMoveNode(String nodeId, String parentId, String nextSiblingId)
throws PortalException {
return this.canMoveNode(this.getNode(nodeId), this.getNode(parentId), nextSiblingId);
}
protected boolean canMoveNode(
IUserLayoutNodeDescription node,
IUserLayoutNodeDescription parent,
String nextSiblingId)
throws PortalException {
// todo if isFragmentOwner should probably verify both node and parent are part of the
// same layout fragment as the fragment owner to insure a misbehaving front-end doesn't
// do an improper operation.
// are we moving to a new parent?
if (!getParentId(node.getId()).equals(parent.getId()))
return (isFragmentOwner || node.isMoveAllowed())
&& canAddNode(node, parent, nextSiblingId);
// same parent. which direction are we moving?
Document uld = this.getUserLayoutDOM();
Element parentE = uld.getElementById(parent.getId());
Element child = (Element) parentE.getFirstChild();
int idx = 0;
int nodeIdx = -1;
int sibIdx = -1;
while (child != null) {
String id = child.getAttribute(Constants.ATT_ID);
if (id.equals(node.getId())) nodeIdx = idx;
if (id.equals(nextSiblingId)) sibIdx = idx;
idx++;
child = (Element) child.getNextSibling();
}
if (nodeIdx == -1
|| // couldn't find node
(nextSiblingId != null && sibIdx == -1)) // couldn't find sibling
return false;
if (nodeIdx < sibIdx
|| // moving right
sibIdx == -1) // appending to end
return canMoveRight(node.getId(), nextSiblingId);
return canMoveLeft(node.getId(), nextSiblingId);
}
private boolean canMoveRight(String nodeId, String targetNextSibId) throws PortalException {
IUserLayoutNodeDescription node = getNode(nodeId);
Enumeration sibIds = getVisibleChildIds(getParentId(nodeId));
List sibs = Collections.list(sibIds);
for (int idx = sibs.indexOf(nodeId) + 1; idx > 0 && idx < sibs.size(); idx++) {
String nextSibId = (String) sibs.get(idx);
IUserLayoutNodeDescription next = getNode(nextSibId);
if (nextSibId != null && next.getId().equals(targetNextSibId)) return true;
else if (!isFragmentOwner && !MovementRules.canHopRight(node, next)) return false;
}
if (targetNextSibId == null) // made it to end of sib list and
return true; // that is the desired location
return false; // oops never found the sib. Should never happen.
}
private boolean canMoveLeft(String nodeId, String targetNextSibId) throws PortalException {
IUserLayoutNodeDescription node = getNode(nodeId);
Enumeration sibIds = getVisibleChildIds(getParentId(nodeId));
List sibs = Collections.list(sibIds);
for (int idx = sibs.indexOf(nodeId) - 1; idx >= 0; idx--) {
String prevSibId = (String) sibs.get(idx);
IUserLayoutNodeDescription prev = getNode(prevSibId);
if (!isFragmentOwner && !MovementRules.canHopLeft(node, prev)) return false;
if (targetNextSibId != null && prev.getId().equals(targetNextSibId)) return true;
}
return false; // oops never found the sib
}
public boolean canDeleteNode(String nodeId) throws PortalException {
return canDeleteNode(this.getNode(nodeId));
}
/**
* Returns true if the node exists in the underlying DOM model and it does not contain a
* 'deleteAllowed' attribute with a value of 'false'.
*/
protected boolean canDeleteNode(IUserLayoutNodeDescription node) throws PortalException {
if (node == null) return false;
// todo if isFragmentOwner should probably verify node is part of the
// same layout fragment as the fragment owner to insure a misbehaving front-end doesn't
// do an improper operation.
return isFragmentOwner || node.isDeleteAllowed();
}
/**
* Returns true if we are dealing with a fragment layout or if editing of attributes is allowed,
* or the node is a channel since ad-hoc parameters can always be added.
*/
public boolean canUpdateNode(IUserLayoutNodeDescription node) {
if (node == null) return false;
return isFragmentOwner
|| node.isEditAllowed()
|| node instanceof IUserLayoutChannelDescription;
}
/**
* Unsupported operation in DLM. This feature is handled by pluggable processors in the DLM
* processing pipe. See properties/dlmContext.xml.
*/
public void markAddTargets(IUserLayoutNodeDescription node) {
throw new UnsupportedOperationException(
"Use an appropriate " + "processor for adding targets.");
}
/**
* Unsupported operation in DLM. This feature is handled by pluggable processors in the DLM
* processing pipe. See properties/dlmContext.xml.
*/
public void markMoveTargets(String nodeId) throws PortalException {
throw new UnsupportedOperationException(
"Use an appropriate " + "processor for adding targets.");
}
public String getParentId(String nodeId) throws PortalException {
Document uld = this.getUserLayoutDOM();
Element nelement = uld.getElementById(nodeId);
if (nelement != null) {
Node parent = nelement.getParentNode();
if (parent != null) {
if (parent.getNodeType() != Node.ELEMENT_NODE) {
throw new PortalException(
"Node with id=\""
+ nodeId
+ "\" is attached to something other then an element node.");
}
Element e = (Element) parent;
return e.getAttribute("ID");
}
return null;
}
throw new PortalException(
"Node with id=\""
+ nodeId
+ "\" doesn't exist. Occurred in layout for "
+ owner.getAttribute(IPerson.USERNAME)
+ ".");
}
public String getNextSiblingId(String nodeId) throws PortalException {
Document uld = this.getUserLayoutDOM();
Element nelement = uld.getElementById(nodeId);
if (nelement != null) {
Node nsibling = nelement.getNextSibling();
// scroll to the next element node
while (nsibling != null && nsibling.getNodeType() != Node.ELEMENT_NODE) {
nsibling = nsibling.getNextSibling();
}
if (nsibling != null) {
Element e = (Element) nsibling;
return e.getAttribute("ID");
}
return null;
}
throw new PortalException(
"Node with id=\""
+ nodeId
+ "\" doesn't exist. Occurred "
+ "in layout for "
+ owner.getAttribute(IPerson.USERNAME)
+ ".");
}
public String getPreviousSiblingId(String nodeId) throws PortalException {
Document uld = this.getUserLayoutDOM();
Element nelement = uld.getElementById(nodeId);
if (nelement != null) {
Node nsibling = nelement.getPreviousSibling();
// scroll to the next element node
while (nsibling != null && nsibling.getNodeType() != Node.ELEMENT_NODE) {
nsibling = nsibling.getNextSibling();
}
if (nsibling != null) {
Element e = (Element) nsibling;
return e.getAttribute("ID");
}
return null;
}
throw new PortalException(
"Node with id=\""
+ nodeId
+ "\" doesn't exist. Occurred in layout for "
+ owner.getAttribute(IPerson.USERNAME)
+ ".");
}
public Enumeration<String> getChildIds(String nodeId) throws PortalException {
return getChildIds(nodeId, false);
}
private Enumeration<String> getVisibleChildIds(String nodeId) throws PortalException {
return getChildIds(nodeId, true);
}
private Enumeration<String> getChildIds(String nodeId, boolean visibleOnly)
throws PortalException {
Vector<String> v = new Vector<String>();
IUserLayoutNodeDescription node = getNode(nodeId);
if (node instanceof IUserLayoutFolderDescription) {
Document uld = this.getUserLayoutDOM();
Element felement = uld.getElementById(nodeId);
for (Node n = felement.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n.getNodeType() == Node.ELEMENT_NODE
&& (visibleOnly == false
|| (visibleOnly == true
&& ((Element) n)
.getAttribute(Constants.ATT_HIDDEN)
.equals("false")))) {
Element e = (Element) n;
if (e.getAttribute("ID") != null) {
v.add(e.getAttribute("ID"));
}
}
}
}
return v.elements();
}
@Override
public String getCacheKey() {
return this.cacheKey;
}
/**
* This is outright cheating! We're supposed to analyze the user layout tree and return a key
* that corresponds uniquely to the composition and the structure of the tree. Here we just
* return a different key whenever anything changes. So if one was to move a node back and
* forth, the key would always never (almost) come back to the original value, even though the
* changes to the user layout are cyclic.
*/
private void updateCacheKey() {
this.cacheKey = Long.toString(rnd.nextLong());
}
public int getLayoutId() {
return profile.getLayoutId();
}
/**
* Returns the subscribe ID of a channel having the passed in functional name or null if it
* can't find such a channel in the layout.
*/
@Override
public String getSubscribeId(String fname) {
final Document userLayout = this.getUserLayoutDOM();
return new PortletSubscribeIdResolver(fname).traverseDocument(userLayout);
}
public String getSubscribeId(String parentFolderId, String fname) {
final Map<String, String> variables = new HashMap<String, String>();
variables.put("parentFolderId", parentFolderId);
variables.put("fname", fname);
final Document userLayout = this.getUserLayoutDOM();
final Element fnameNode =
this.xpathOperations.evaluate(
"//folder[@ID=$parentFolderId]/descendant::channel[@fname=$fname]",
variables,
userLayout,
XPathConstants.NODE);
if (fnameNode != null) {
return fnameNode.getAttribute("ID");
}
return null;
}
/* (non-Javadoc)
* @see org.apereo.portal.layout.IUserLayoutManager#getUserLayout()
*/
public IUserLayout getUserLayout() throws PortalException {
// Copied from SimpleLayoutManager since our layouts are regular
// simple layouts, ie Documents.
return new SimpleLayout(
this.getDistributedUserLayout(),
String.valueOf(profile.getLayoutId()));
}
/* Returns the ID attribute of the root folder of the layout. This folder
* is defined to be the single child of the top most "layout" Element.
*
* @see org.apereo.portal.layout.IUserLayoutManager#getRootFolderId()
* @see org.apereo.portal.layout.dlm.RootLocator
*/
public String getRootFolderId() {
if (rootNodeId == null) {
Document layout = getUserLayoutDOM();
Element rootNode =
this.xpathOperations.evaluate("//layout/folder", layout, XPathConstants.NODE);
if (rootNode == null
|| !rootNode.getAttribute(Constants.ATT_TYPE)
.equals(Constants.ROOT_FOLDER_ID)) {
LOG.error(
"Unable to locate root node in layout of "
+ owner.getAttribute(IPerson.USERNAME)
+ ". Resetting corrupted layout.");
resetLayout((String) null);
rootNode =
this.xpathOperations.evaluate(
"//layout/folder", layout, XPathConstants.NODE);
if (rootNode == null
|| !rootNode.getAttribute(Constants.ATT_TYPE)
.equals(Constants.ROOT_FOLDER_ID)) {
throw new PortalException(
"Corrupted layout detected for "
+ owner.getAttribute(IPerson.USERNAME)
+ " and resetting layout failed.");
}
}
rootNodeId = rootNode.getAttribute("ID");
}
return rootNodeId;
}
/*
* (non-Javadoc)
*
* @see org.apereo.portal.layout.IUserLayoutManager#getDepth(java.lang.String)
*/
public int getDepth(String nodeId) throws PortalException {
// can't see what it calling this anywhere so ignoring for now.
// TODO waiting to hear back from peter/michael
return 0;
}
/* Return an implementation of IUserLayoutNodeDescription appropriate for
* the type of node indicated. Currently, the only two types supported are
* IUserLayoutNodeDescription.FOLDER and LayoutNodeType.PORTLET.
*
* @see org.apereo.portal.layout.IUserLayoutManager#createNodeDescription(int)
*/
@Override
public IUserLayoutNodeDescription createNodeDescription(LayoutNodeType nodeType)
throws PortalException {
if (nodeType == LayoutNodeType.FOLDER) {
return new UserLayoutFolderDescription();
}
return new ChannelDescription();
}
/**
* Resets the layout of the user with the specified user id if the current user is an
* administrator or a member of any administrative sub-group. Has no effect if these
* requirements are not met.
*
* @return true if layout was reset, false otherwise.
* @param loginId
*/
public boolean resetLayout(String loginId) {
boolean resetSuccess = false;
boolean resetCurrentUserLayout = (null == loginId);
if (resetCurrentUserLayout || (!resetCurrentUserLayout && AdminEvaluator.isAdmin(owner))) {
if (LOG.isDebugEnabled()) {
LOG.debug("Reset layout requested for user with id " + loginId + ".");
}
int portalID = IPerson.UNDEFINED_ID;
IPerson person = null;
if (resetCurrentUserLayout || loginId.equals(owner.getAttribute(IPerson.USERNAME))) {
person = owner;
portalID = owner.getID();
} else {
// need to get the portal id
person = PersonFactory.createPerson();
person.setAttribute(IPerson.USERNAME, loginId);
try {
portalID = userIdentityStore.getPortalUID(person);
person.setID(portalID);
} catch (Exception e) {
// ignore since the store will log the problem
}
}
if (portalID != IPerson.UNDEFINED_ID) {
resetSuccess = resetLayout(person);
}
} else {
LOG.error(
"Layout reset requested for user "
+ loginId
+ " by "
+ owner.getID()
+ " who is not an administrative user.");
}
return resetSuccess;
}
/** Resets the layout of the specified user. */
private boolean resetLayout(IPerson person) {
final String userName = person.getUserName();
if (PersonFactory.GUEST_USERNAMES.contains(userName)) {
throw new IllegalArgumentException("CANNOT RESET LAYOUT FOR A GUEST USER: " + person);
}
LOG.warn("Resetting user layout for: " + userName, new Throwable());
boolean layoutWasReset = false;
/*
* is the person being reset a fragment owner? Can't use the
* isFramentOwner variable in this class since we could be resetting
* another user's layout.
*/
if (this.distributedLayoutStore.isFragmentOwner(person)) {
// set template user override so reload of layout comes from
// fragment template user
person.setAttribute(
org.apereo.portal.Constants.TEMPLATE_USER_NAME_ATT,
FragmentDefinition.getDefaultLayoutOwnerId());
}
try {
userIdentityStore.removePortalUID(person.getID());
userIdentityStore.getPortalUID(person, true);
// see if the current user was the one to reset their layout and if
// so we need to refresh our local copy of their layout
if (person == owner) {
this.layoutCachingService.removeCachedLayout(person, profile);
updateCacheKey();
getUserLayoutDOM();
}
//if (isFragmentOwner)
//{
//
// store.updateOwnerLayout(person);
//}
layoutWasReset = true;
} catch (Exception e) {
LOG.error(
"Unable to reset layout for " + person.getAttribute(IPerson.USERNAME) + ".", e);
}
return layoutWasReset;
}
public IUserLayoutNodeDescription createNodeDescription(Element node) throws PortalException {
String type = node.getNodeName();
if (type.equals(Constants.ELM_CHANNEL)) {
return new ChannelDescription(node);
} else if (type.equals(Constants.ELM_FOLDER)) {
return new UserLayoutFolderDescription(node);
} else {
throw new PortalException("Given XML Element is not a channel!");
}
}
}