/** * 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.remoting; import static org.apereo.portal.layout.node.IUserLayoutNodeDescription.LayoutNodeType.FOLDER; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.annotation.PostConstruct; import javax.portlet.WindowState; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.apache.commons.beanutils.BeanPredicate; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.functors.EqualPredicate; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.apereo.portal.IUserIdentityStore; import org.apereo.portal.PortalException; import org.apereo.portal.UserPreferencesManager; import org.apereo.portal.fragment.subscribe.IUserFragmentSubscription; import org.apereo.portal.fragment.subscribe.dao.IUserFragmentSubscriptionDao; import org.apereo.portal.groups.IEntity; 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.IUserLayoutStore; import org.apereo.portal.layout.PortletSubscribeIdResolver; import org.apereo.portal.layout.dlm.Constants; import org.apereo.portal.layout.dlm.DistributedUserLayout; import org.apereo.portal.layout.dlm.UserPrefsHandler; 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.UserLayoutChannelDescription; import org.apereo.portal.layout.node.UserLayoutFolderDescription; import org.apereo.portal.portlet.om.IPortletDefinition; import org.apereo.portal.portlet.om.IPortletWindow; import org.apereo.portal.portlet.registry.IPortletDefinitionRegistry; import org.apereo.portal.portlet.registry.IPortletWindowRegistry; import org.apereo.portal.portlets.favorites.FavoritesUtils; import org.apereo.portal.security.IAuthorizationPrincipal; import org.apereo.portal.security.IPermission; import org.apereo.portal.security.IPerson; import org.apereo.portal.security.PermissionHelper; import org.apereo.portal.security.PersonFactory; import org.apereo.portal.security.provider.RestrictedPerson; import org.apereo.portal.services.AuthorizationService; import org.apereo.portal.services.GroupService; import org.apereo.portal.user.IUserInstance; import org.apereo.portal.user.IUserInstanceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.support.RequestContextUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; /** * Provides targets for AJAX preference setting calls. * */ @Controller @RequestMapping("/layout") public class UpdatePreferencesServlet { private static final String TAB_GROUP_PARAMETER = "tabGroup"; // matches incoming JS private static final String TAB_GROUP_DEFAULT = "DEFAULT_TABGROUP"; // matches default in structure transform protected final Logger logger = LoggerFactory.getLogger(getClass()); private IPortletDefinitionRegistry portletDefinitionRegistry; private IUserIdentityStore userIdentityStore; private IUserFragmentSubscriptionDao userFragmentInfoDao; private IUserInstanceManager userInstanceManager; private IStylesheetUserPreferencesService stylesheetUserPreferencesService; private IUserLayoutStore userLayoutStore; private MessageSource messageSource; private IPortletWindowRegistry portletWindowRegistry; @Value("${org.apereo.portal.layout.dlm.remoting.addedWindowState:null}") private String addedPortletWindowState; private WindowState addedWindowState; @PostConstruct private void initAddedPortletWindowState() { if (addedPortletWindowState != null && !"null".equalsIgnoreCase(addedPortletWindowState) && !addedPortletWindowState.isEmpty()) { addedWindowState = new WindowState(addedPortletWindowState); } } @Autowired public void setUserLayoutStore(IUserLayoutStore userLayoutStore) { this.userLayoutStore = userLayoutStore; } @Autowired public void setStylesheetUserPreferencesService( IStylesheetUserPreferencesService stylesheetUserPreferencesService) { this.stylesheetUserPreferencesService = stylesheetUserPreferencesService; } @Autowired public void setPortletDefinitionRegistry(IPortletDefinitionRegistry portletDefinitionRegistry) { this.portletDefinitionRegistry = portletDefinitionRegistry; } @Autowired public void setUserIdentityStore(IUserIdentityStore userStore) { this.userIdentityStore = userStore; } @Autowired public void setUserFragmentInfoDao(IUserFragmentSubscriptionDao userFragmentInfoDao) { this.userFragmentInfoDao = userFragmentInfoDao; } @Autowired public void setUserInstanceManager(IUserInstanceManager userInstanceManager) { this.userInstanceManager = userInstanceManager; } @Autowired public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } @Autowired public void setPortletWindowRegistry(IPortletWindowRegistry portletWindowRegistry) { this.portletWindowRegistry = portletWindowRegistry; } // default tab name protected static final String DEFAULT_TAB_NAME = "New Tab"; /** * Remove an element from the layout. * * @param request * @param response * @return * @throws IOException */ @RequestMapping(method = RequestMethod.POST, params = "action=removeElement") public ModelAndView removeElement(HttpServletRequest request, HttpServletResponse response) throws IOException { IUserInstance ui = userInstanceManager.getUserInstance(request); IPerson per = getPerson(ui, response); UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); try { // if the element ID starts with the fragment prefix and is a folder, // attempt first to treat it as a pulled fragment subscription String elementId = request.getParameter("elementID"); if (elementId != null && elementId.startsWith(Constants.FRAGMENT_ID_USER_PREFIX) && ulm.getNode(elementId) instanceof org.apereo.portal.layout.node.UserLayoutFolderDescription) { removeSubscription(per, elementId, ulm); } else { // Delete the requested element node. This code is the same for // all node types, so we can just have a generic action. if (!ulm.deleteNode(elementId)) { logger.info( "Failed to remove element ID {} from layout root folder ID {}, delete node returned false", elementId, ulm.getRootFolderId()); response.sendError(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "error", getMessage( "error.element.update", "Unable to update element", RequestContextUtils.getLocale(request)))); } } ulm.saveUserLayout(); return new ModelAndView("jsonView", Collections.EMPTY_MAP); } catch (PortalException e) { return handlePersistError(request, response, e); } } /** * Remove the first element with the provided fname from the layout. * * @param request HttpServletRequest * @param response HttpServletResponse * @param fname fname of the portlet to remove from the layout * @return json response * @throws IOException if the person cannot be retrieved */ @RequestMapping(method = RequestMethod.POST, params = "action=removeByFName") public ModelAndView removeByFName( HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "fname", required = true) String fname ) throws IOException { IUserInstance ui = userInstanceManager.getUserInstance(request); IPerson per = getPerson(ui, response); UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); try { String elementId = ulm.getUserLayout().findNodeId(new PortletSubscribeIdResolver(fname)); if (elementId != null && elementId.startsWith(Constants.FRAGMENT_ID_USER_PREFIX) && ulm.getNode(elementId) instanceof org.apereo.portal.layout.node.UserLayoutFolderDescription) { removeSubscription(per, elementId, ulm); } else if (elementId != null) { // Delete the requested element node. This code is the same for // all node types, so we can just have a generic action. if (!ulm.deleteNode(elementId)) { logger.info( "Failed to remove element ID {} from layout root folder ID {}, delete node returned false", elementId, ulm.getRootFolderId()); response.setStatus(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "error", getMessage( "error.element.update", "Unable to update element", RequestContextUtils.getLocale(request)))); } } else { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return null; } ulm.saveUserLayout(); return new ModelAndView("jsonView", Collections.EMPTY_MAP); } catch (PortalException e) { return handlePersistError(request, response, e); } } /** * Subscribe a user to a pre-formatted tab (pulled DLM fragment). * * @param request * @param response * @return * @throws IOException */ @RequestMapping(method = RequestMethod.POST, params = "action=subscribeToTab") public ModelAndView subscribeToTab(HttpServletRequest request, HttpServletResponse response) throws IOException { IUserInstance ui = userInstanceManager.getUserInstance(request); IPerson per = getPerson(ui, response); UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); // Get the fragment owner's name from the request and construct // an IPerson object representing that user String fragmentOwnerName = request.getParameter("sourceID"); if (StringUtils.isBlank(fragmentOwnerName)) { logger.warn("Attempted to subscribe to tab with null owner ID"); response.sendError(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView( "jsonView", Collections.singletonMap( "error", "Attempted to subscribe to tab with null owner ID")); } RestrictedPerson fragmentOwner = PersonFactory.createRestrictedPerson(); fragmentOwner.setUserName(fragmentOwnerName); // Mark the currently-authenticated user as subscribed to this fragment. // If an inactivated fragment registration already exists, update it // as an active subscription. Otherwise, create a new fragment // subscription. IUserFragmentSubscription userFragmentInfo = userFragmentInfoDao.getUserFragmentInfo(per, fragmentOwner); if (userFragmentInfo == null) { userFragmentInfo = userFragmentInfoDao.createUserFragmentInfo(per, fragmentOwner); } else { userFragmentInfo.setActive(true); userFragmentInfoDao.updateUserFragmentInfo(userFragmentInfo); } try { // reload user layout and stylesheet to incorporate new DLM fragment ulm.loadUserLayout(true); // get the target node this new tab should be moved after String destinationId = request.getParameter("elementID"); // get the user layout for the currently-authenticated user int uid = userIdentityStore.getPortalUID(fragmentOwner, false); final DistributedUserLayout userLayout = userLayoutStore.getUserLayout(per, upm.getUserProfile()); Document layoutDocument = userLayout.getLayout(); // attempt to find the new subscribed tab in the layout so we can // move it StringBuilder expression = new StringBuilder("//folder[@type='root']/folder[starts-with(@ID,'") .append(Constants.FRAGMENT_ID_USER_PREFIX) .append(uid) .append("')]"); XPathFactory fac = XPathFactory.newInstance(); XPath xpath = fac.newXPath(); NodeList nodes = (NodeList) xpath.evaluate( expression.toString(), layoutDocument, XPathConstants.NODESET); String sourceId = nodes.item(0).getAttributes().getNamedItem("ID").getTextContent(); ulm.moveNode(sourceId, ulm.getParentId(destinationId), destinationId); ulm.saveUserLayout(); return new ModelAndView("jsonView", Collections.singletonMap("tabId", sourceId)); } catch (XPathExpressionException e) { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return new ModelAndView("jsonView", Collections.singletonMap("error", "Xpath error")); } catch (PortalException e) { return handlePersistError(request, response, e); } } /** * Moves the portlet either before nextNodeId or after previousNodeId as appropriate. * * @param request HttpRequest * @param response HttpResponse * @param sourceId nodeId to move * @param previousNodeId if nextNodeId is not blank, moves portlet to end of list previousNodeId * is in * @param nextNodeId nodeId to insert sourceId before. * @return */ @RequestMapping(method = RequestMethod.POST, params = "action=movePortletAjax") public ModelAndView movePortletAjax( HttpServletRequest request, HttpServletResponse response, @RequestParam String sourceId, @RequestParam String previousNodeId, @RequestParam String nextNodeId) { final Locale locale = RequestContextUtils.getLocale(request); boolean success = false; if (StringUtils.isNotBlank(nextNodeId)) { success = moveElementInternal(request, sourceId, nextNodeId, "insertBefore"); } else { success = moveElementInternal(request, sourceId, previousNodeId, "appendAfter"); } if (success) { return new ModelAndView( "jsonView", Collections.singletonMap( "response", getMessage( "success.move.element", "Element moved successfully", locale))); } else { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "response", getMessage("error.move.element", "Error moving element.", locale))); } } /** * Move a portlet to another location on the tab. * * <p>This deprecated method is replaced by the method/action "moveElement". The code is the * same, but the naming better abstracts the action. This method is here for backwards * compatibility with anything using the "movePortlet" action of the API. * * <p>Used by Respondr UI when moving portlets around in content area. uPortal 4.2 and prior * behavior: * * <ul> * <li>If destination is a tab either adds to end of 1st column or if no columns, creates one * and adds it. AFAIK this was not actually used by the UI. * <li>If target is a column (2 down from root), portlet always added to end of column. Used * by UI to drop portlet into empty column (UI did insertBefore with elementId=columnId) * <li>If method=insertBefore does insert before elementId (always a portlet in 4.2). * <li>If method=appendAfter does append at end of parent(elementId), result of which is a * column. Used by UI to add to end of column (elementId is last portlet in column). * </ul> * * @param request * @param response * @throws IOException * @throws PortalException * @deprecated - replaced by the method/action "moveElement" */ @RequestMapping(method = RequestMethod.POST, params = "action=movePortlet") @Deprecated public ModelAndView movePortlet( HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "sourceID") String sourceId, @RequestParam String method, @RequestParam(value = "elementID") String destinationId) throws IOException, PortalException { return moveElement(request, response, sourceId, method, destinationId); } /** * Move an element to another location on the tab. * * <p>Used by Respondr UI when moving portlets around in content area. Will be made more generic * to support ngPortal UI which supports arbitrary nesting of folders. When that code is merged * in, the behavior of this method will need to change slightly (make sure movePortlet behavior * doesn't change though). Current behavior: * * <ul> * <li>If destination is a tab either adds to end of 1st column or if no columns, creates one * and adds it. AFAIK this was not actually used by the UI. * <li>If target is a column (2 down from root), portlet always added to end of column. Used * by UI to drop portlet into empty column (UI did insertBefore with elementId=columnId) * <li>If method=insertBefore does insert before elementId (always a portlet in 4.2). * <li>If method=appendAfter does append at end of parent(elementId), result of which is a * column. Used by UI to add to end of column (elementId is last portlet in column). * </ul> * * @param request * @param response * @param sourceId id of the element to move * @param method insertBefore or appendAfter * @param destinationId Id of element. If a tab, sourceID added to end of a folder/column in the * tab. If a folder, sourceID added to the end of the folder. Otherwise sourceID added * before elementID. * @throws IOException * @throws PortalException */ @RequestMapping(method = RequestMethod.POST, params = "action=moveElement") public ModelAndView moveElement( HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "sourceID") String sourceId, @RequestParam String method, @RequestParam(value = "elementID") String destinationId) throws IOException, PortalException { final Locale locale = RequestContextUtils.getLocale(request); if (moveElementInternal(request, sourceId, destinationId, method)) { return new ModelAndView( "jsonView", Collections.singletonMap( "response", getMessage( "success.move.element", "Element moved successfully", locale))); } else { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "response", getMessage("error.move.element", "Error moving element", locale))); } } /** * Change the number of columns on a specified tab. In the event that the user is decreasing the * number of columns, extra columns will be stripped from the right-hand side. Any channels in * these columns will be moved to the bottom of the last preserved column. * * @param widths array of column widths * @param deleted array of deleted column IDs * @param acceptor not sure what this is * @param request HttpRequest * @param response HttpResponse * @throws IOException * @throws PortalException */ @RequestMapping(method = RequestMethod.POST, params = "action=changeColumns") public ModelAndView changeColumns( HttpServletRequest request, HttpServletResponse response, @RequestParam("tabId") String tabId, @RequestParam("widths[]") String[] widths, @RequestParam(value = "deleted[]", required = false) String[] deleted, @RequestParam(value = "acceptor", required = false) String acceptor) throws IOException, PortalException { IUserInstance ui = userInstanceManager.getUserInstance(request); IPerson per = getPerson(ui, response); UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); int newColumnCount = widths.length; // build a list of the current columns for this tab Enumeration<String> columns = ulm.getChildIds(tabId); List<String> columnList = new ArrayList<String>(); while (columns.hasMoreElements()) { columnList.add(columns.nextElement()); } int oldColumnCount = columnList.size(); Map<String, Object> model = new HashMap<String, Object>(); // if the new layout has more columns if (newColumnCount > oldColumnCount) { List<String> newColumnIds = new ArrayList<String>(); for (int i = columnList.size(); i < newColumnCount; i++) { // create new column element IUserLayoutFolderDescription newColumn = new UserLayoutFolderDescription(); newColumn.setName("Column"); newColumn.setId("tbd"); newColumn.setFolderType(IUserLayoutFolderDescription.REGULAR_TYPE); newColumn.setHidden(false); newColumn.setUnremovable(false); newColumn.setImmutable(false); // add the column to our layout IUserLayoutNodeDescription node = ulm.addNode(newColumn, tabId, null); newColumnIds.add(node.getId()); model.put("newColumnIds", newColumnIds); columnList.add(node.getId()); } } // if the new layout has fewer columns else if (deleted != null && deleted.length > 0) { if (columnList.size() != widths.length + deleted.length) { // TODO: error? } for (String columnId : deleted) { // move all channels in the current column to the last valid column Enumeration channels = ulm.getChildIds(columnId); while (channels.hasMoreElements()) { ulm.addNode(ulm.getNode((String) channels.nextElement()), acceptor, null); } // delete the column from the user's layout ulm.deleteNode(columnId); columnList.remove(columnId); } } int count = 0; for (String columnId : columnList) { this.stylesheetUserPreferencesService.setLayoutAttribute( request, PreferencesScope.STRUCTURE, columnId, "width", widths[count] + "%"); try { // This sets the column attribute in memory but doesn't persist it. Comment says saves changes "prior to persisting" Element folder = ulm.getUserLayoutDOM().getElementById(columnId); UserPrefsHandler.setUserPreference(folder, "width", per); } catch (Exception e) { logger.error("Error saving new column widths", e); } count++; } try { ulm.saveUserLayout(); } catch (PortalException e) { logger.warn("Error saving layout", e); } return new ModelAndView("jsonView", model); } /** * Move a tab left or right. * * @param sourceId node ID of tab to move * @param method insertBefore or appendAfter. If appendAfter, tab is added as last tab (parent * of destinationId). * @param destinationId insertBefore: node ID of tab to move sourceId before. insertAfter: node * ID of another tab * @param request * @param response * @throws PortalException * @throws IOException */ @RequestMapping(method = RequestMethod.POST, params = "action=moveTab") public ModelAndView moveTab( HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "sourceID") String sourceId, @RequestParam String method, @RequestParam(value = "elementID") String destinationId) throws IOException { IUserInstance ui = userInstanceManager.getUserInstance(request); UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); final Locale locale = RequestContextUtils.getLocale(request); // If we're moving this element before another one, we need // to know what the target is. If there's no target, just // assume we're moving it to the very end of the list. String siblingId = null; if ("insertBefore".equals(method)) siblingId = destinationId; try { // move the node as requested and save the layout if (!ulm.moveNode(sourceId, ulm.getParentId(destinationId), siblingId)) { logger.warn("Failed to move tab in user layout. moveNode returned false"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "response", getMessage( "error.move.tab", "There was an issue moving the tab, please refresh the page and try again.", locale))); } ulm.saveUserLayout(); } catch (PortalException e) { return handlePersistError(request, response, e); } return new ModelAndView( "jsonView", Collections.singletonMap( "response", getMessage("success.move.tab", "Tab moved successfully", locale))); } @RequestMapping(method = RequestMethod.POST, params = "action=addFavorite") public ModelAndView addFavorite( @RequestParam String channelId, HttpServletRequest request, HttpServletResponse response) throws IOException { final IUserInstance ui = userInstanceManager.getUserInstance(request); final IPerson person = getPerson(ui, response); final IPortletDefinition pdef = portletDefinitionRegistry.getPortletDefinition(channelId); final Locale locale = RequestContextUtils.getLocale(request); final IAuthorizationPrincipal authPrincipal = this.getUserPrincipal(person.getUserName()); final String targetString = PermissionHelper.permissionTargetIdForPortletDefinition(pdef); if (!authPrincipal.hasPermission( IPermission.PORTAL_SYSTEM, IPermission.PORTLET_FAVORITE_ACTIVITY, targetString)) { logger.warn( "Unauthorized attempt to favorite portlet '{}' through the REST API by user '{}'", pdef.getFName(), person.getUserName()); response.setStatus(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "response", getMessage( "error.favorite.not.permitted", "Favorite not permitted", locale))); } final UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); final IUserLayoutManager ulm = upm.getUserLayoutManager(); final IUserLayoutChannelDescription channel = new UserLayoutChannelDescription(pdef); //get favorite tab final String favoriteTabNodeId = FavoritesUtils.getFavoriteTabNodeId(ulm.getUserLayout()); if (favoriteTabNodeId != null) { //add portlet to favorite tab final IUserLayoutNodeDescription node = addNodeToTab(ulm, channel, favoriteTabNodeId); if (node == null) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "response", getMessage( "error.add.portlet.in.tab", "Can''t add a new favorite", locale))); } try { // save the user's layout ulm.saveUserLayout(); } catch (PortalException e) { return handlePersistError(request, response, e); } //document success for notifications final Map<String, String> model = new HashMap<String, String>(); final String channelTitle = channel.getTitle(); model.put( "response", getMessage( "favorites.added.favorite", channelTitle, "Added " + channelTitle + " as a favorite.", locale)); model.put("newNodeId", node.getId()); return new ModelAndView("jsonView", model); } else { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView( "jsonView", Collections.singletonMap( "response", getMessage( "error.finding.favorite.tab", "Can''t find favorite tab", locale))); } } /** * This method removes the channelId specified from favorites. Note that even if you pass in the * layout channel id, it will always remove from the favorites. * * @param channelId The long channel ID that is used to determine which fname to remove from * favorites * @param request * @param response * @return returns a mav object with a response attribute for noty * @throws IOException if it has problem reading the layout file. */ @RequestMapping(method = RequestMethod.POST, params = "action=removeFavorite") public ModelAndView removeFavorite( @RequestParam String channelId, HttpServletRequest request, HttpServletResponse response) throws IOException { UserPreferencesManager upm = (UserPreferencesManager) userInstanceManager.getUserInstance(request).getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); final Locale locale = RequestContextUtils.getLocale(request); IPortletDefinition portletDefinition = portletDefinitionRegistry.getPortletDefinition(channelId); if (portletDefinition != null && StringUtils.isNotBlank(portletDefinition.getFName())) { String functionalName = portletDefinition.getFName(); List<IUserLayoutNodeDescription> favoritePortlets = FavoritesUtils.getFavoritePortlets(ulm.getUserLayout()); //search for the favorite to delete EqualPredicate nameEqlPredicate = new EqualPredicate(functionalName); Object result = CollectionUtils.find( favoritePortlets, new BeanPredicate("functionalName", nameEqlPredicate)); if (result != null && result instanceof UserLayoutChannelDescription) { UserLayoutChannelDescription channelDescription = (UserLayoutChannelDescription) result; try { if (!ulm.deleteNode(channelDescription.getChannelSubscribeId())) { logger.warn( "Error deleting the node" + channelId + "from favorites for user " + (upm.getPerson() == null ? "unknown" : upm.getPerson().getID())); response.setStatus(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "response", getMessage( "error.remove.favorite", "Can''t remove favorite", locale))); } // save the user's layout ulm.saveUserLayout(); } catch (PortalException e) { return handlePersistError(request, response, e); } //document success for notifications Map<String, String> model = new HashMap<String, String>(); model.put( "response", getMessage( "success.remove.portlet", "Removed from Favorites successfully", locale)); return new ModelAndView("jsonView", model); } } // save the user's layout ulm.saveUserLayout(); return new ModelAndView( "jsonView", Collections.singletonMap( "response", getMessage("error.finding.favorite", "Can''t find favorite", locale))); } /** * Add a new channel. * * @param request * @param response * @throws IOException * @throws PortalException */ @RequestMapping(method = RequestMethod.POST, params = "action=addPortlet") public ModelAndView addPortlet(HttpServletRequest request, HttpServletResponse response) throws IOException, PortalException { IUserInstance ui = userInstanceManager.getUserInstance(request); UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); final Locale locale = RequestContextUtils.getLocale(request); // gather the parameters we need to move a channel String destinationId = request.getParameter("elementID"); String sourceId = request.getParameter("channelID"); String method = request.getParameter("position"); String fname = request.getParameter("fname"); if (destinationId == null) { String tabName = request.getParameter("tabName"); if (tabName != null) { destinationId = getTabIdFromName(ulm.getUserLayout(), tabName); } } IPortletDefinition definition = null; if (sourceId != null) definition = portletDefinitionRegistry.getPortletDefinition(sourceId); else if (fname != null) definition = portletDefinitionRegistry.getPortletDefinitionByFname(fname); else { logger.error("SourceId or fname invalid when adding a portlet"); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView( "jsonView", Collections.singletonMap("error", "SourceId or fname invalid")); } IUserLayoutChannelDescription channel = new UserLayoutChannelDescription(definition); IUserLayoutNodeDescription node = null; if (isTab(ulm, destinationId)) { node = addNodeToTab(ulm, channel, destinationId); } else { boolean isInsert = method != null && method.equals("insertBefore"); //If neither an insert or type folder - Can't "insert into" non-folder if (!(isInsert || isFolder(ulm, destinationId))) { logger.error("Cannot insert into portlet element"); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView( "jsonView", Collections.singletonMap("error", "Cannot insert into portlet element")); } String siblingId = isInsert ? destinationId : null; String target = isInsert ? ulm.getParentId(destinationId) : destinationId; // move the channel into the column node = ulm.addNode(channel, target, siblingId); } if (node == null) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "error", getMessage("error.add.element", "Unable to add element", locale))); } String nodeId = node.getId(); try { // save the user's layout ulm.saveUserLayout(); if (addedWindowState != null) { IPortletWindow portletWindow = this.portletWindowRegistry.getOrCreateDefaultPortletWindowByFname( request, channel.getFunctionalName()); portletWindow.setWindowState(addedWindowState); this.portletWindowRegistry.storePortletWindow(request, portletWindow); } } catch (PortalException e) { return handlePersistError(request, response, e); } Map<String, String> model = new HashMap<String, String>(); model.put("response", getMessage("success.add.portlet", "Added a new channel", locale)); model.put("newNodeId", nodeId); return new ModelAndView("jsonView", model); } private IUserLayoutNodeDescription addNodeToTab( IUserLayoutManager ulm, IUserLayoutChannelDescription channel, String tabId) { IUserLayoutNodeDescription node = null; Enumeration<String> columns = ulm.getChildIds(tabId); if (columns.hasMoreElements()) { while (columns.hasMoreElements()) { // attempt to add this channel to the column node = ulm.addNode(channel, columns.nextElement(), null); // if it couldn't be added to this column, go on and try the next // one. otherwise, we're set. if (node != null) break; } } else { IUserLayoutFolderDescription newColumn = new UserLayoutFolderDescription(); newColumn.setName("Column"); newColumn.setId("tbd"); newColumn.setFolderType(IUserLayoutFolderDescription.REGULAR_TYPE); newColumn.setHidden(false); newColumn.setUnremovable(false); newColumn.setImmutable(false); // add the column to our layout IUserLayoutNodeDescription col = ulm.addNode(newColumn, tabId, null); // add the channel node = ulm.addNode(channel, col.getId(), null); } return node; } /** * Update the user's preferred skin. * * @param request HTTP Request * @param response HTTP Response * @param skinName name of the Skin * @throws IOException * @throws PortalException */ @RequestMapping(method = RequestMethod.POST, params = "action=chooseSkin") public ModelAndView chooseSkin( HttpServletRequest request, @RequestParam String skinName) throws IOException { this.stylesheetUserPreferencesService.setStylesheetParameter( request, PreferencesScope.THEME, "skin", skinName); return new ModelAndView("jsonView", Collections.EMPTY_MAP); } /** * Add a new tab to the layout. The new tab will be appended to the end of the list and named * with the BLANK_TAB_NAME variable. * * @param request * @throws IOException */ @RequestMapping(method = RequestMethod.POST, params = "action=addTab") public ModelAndView addTab( HttpServletRequest request, HttpServletResponse response, @RequestParam("widths[]") String[] widths) throws IOException { IUserInstance ui = userInstanceManager.getUserInstance(request); IPerson per = getPerson(ui, response); UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); // Verify that the user has permission to add this tab final IAuthorizationPrincipal authPrincipal = this.getUserPrincipal(per.getUserName()); if (!authPrincipal.hasPermission( IPermission.PORTAL_SYSTEM, IPermission.ADD_TAB_ACTIVITY, IPermission.ALL_TARGET)) { logger.warn( "Attempt to add a tab through the REST API by unauthorized user '" + per.getUserName() + "'"); response.sendError(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap("error", "Add tab disabled")); } // construct a brand new tab String id = "tbd"; String tabName = request.getParameter("tabName"); if (StringUtils.isBlank(tabName)) tabName = DEFAULT_TAB_NAME; IUserLayoutFolderDescription newTab = new UserLayoutFolderDescription(); newTab.setName(tabName); newTab.setId(id); newTab.setFolderType(IUserLayoutFolderDescription.REGULAR_TYPE); newTab.setHidden(false); newTab.setUnremovable(false); newTab.setImmutable(false); // add the tab to the layout ulm.addNode(newTab, ulm.getRootFolderId(), null); try { // save the user's layout ulm.saveUserLayout(); } catch (PortalException e) { return handlePersistError(request, response, e); } // get the id of the newly added tab String tabId = newTab.getId(); for (String width : widths) { // create new column element IUserLayoutFolderDescription newColumn = new UserLayoutFolderDescription(); newColumn.setName("Column"); newColumn.setId("tbd"); newColumn.setFolderType(IUserLayoutFolderDescription.REGULAR_TYPE); newColumn.setHidden(false); newColumn.setUnremovable(false); newColumn.setImmutable(false); // add the column to our layout ulm.addNode(newColumn, tabId, null); this.stylesheetUserPreferencesService.setLayoutAttribute( request, PreferencesScope.STRUCTURE, newColumn.getId(), "width", width + "%"); try { // This sets the column attribute in memory but doesn't persist it. Comment says saves changes "prior to persisting" Element folder = ulm.getUserLayoutDOM().getElementById(newColumn.getId()); UserPrefsHandler.setUserPreference(folder, "width", per); } catch (Exception e) { logger.error("Error saving new column widths", e); } } // ## 'tabGroup' value (optional feature) // Set the 'tabGroup' attribute on the folder element that describes // this new tab; use the currently active tabGroup. if (request.getParameter(TAB_GROUP_PARAMETER) != null) { String tabGroup = request.getParameter(TAB_GROUP_PARAMETER).trim(); if (logger.isDebugEnabled()) { logger.debug(TAB_GROUP_PARAMETER + "=" + tabGroup); } if (!TAB_GROUP_DEFAULT.equals(tabGroup) && tabGroup.length() != 0) { // Persists SSUP values to the database this.stylesheetUserPreferencesService.setLayoutAttribute( request, PreferencesScope.STRUCTURE, tabId, TAB_GROUP_PARAMETER, tabGroup); } } try { // save the user's layout ulm.saveUserLayout(); } catch (PortalException e) { return handlePersistError(request, response, e); } return new ModelAndView("jsonView", Collections.singletonMap("tabId", tabId)); } /** * Add a new folder to the layout. * * @param request * @param response * @param targetId - id of the folder node to add the new folder to. By default, the folder will * be inserted after other existing items in the node unless a siblingId is provided. * @param siblingId - if set, insert new folder prior to the node with this id, otherwise simple * insert at the end of the list. * @param attributes - if included, parse the JSON name-value pairs in the body as the * attributes of the folder. These will override the defaults. e.g. : { * "structureAttributes" : {"display" : "row", "other" : "another" }, "attributes" : * {"hidden": "true", "type" : "header-top" } } */ @RequestMapping(method = RequestMethod.POST, params = "action=addFolder") public ModelAndView addFolder( HttpServletRequest request, HttpServletResponse response, @RequestParam("targetId") String targetId, @RequestParam(value = "siblingId", required = false) String siblingId, @RequestBody(required = false) Map<String, Map<String, String>> attributes) { IUserLayoutManager ulm = userInstanceManager .getUserInstance(request) .getPreferencesManager() .getUserLayoutManager(); final Locale locale = RequestContextUtils.getLocale(request); if (!ulm.getNode(targetId).isAddChildAllowed()) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "error", getMessage("error.add.element", "Unable to add element", locale))); } UserLayoutFolderDescription newFolder = new UserLayoutFolderDescription(); newFolder.setHidden(false); newFolder.setImmutable(false); newFolder.setAddChildAllowed(true); newFolder.setFolderType(IUserLayoutFolderDescription.REGULAR_TYPE); // Update the attributes based on the supplied JSON (optional request body name-value pairs) if (attributes != null && !attributes.isEmpty()) { setObjectAttributes(newFolder, request, attributes); } ulm.addNode(newFolder, targetId, siblingId); try { ulm.saveUserLayout(); } catch (PortalException e) { return handlePersistError(request, response, e); } Map<String, Object> model = new HashMap<>(); model.put("response", getMessage("success.add.folder", "Added a new folder", locale)); model.put("folderId", newFolder.getId()); model.put("immutable", newFolder.isImmutable()); return new ModelAndView("jsonView", model); } /** * Attempt to map the attribute values to the given object. * * @param node * @param request * @param attributes */ private void setObjectAttributes( IUserLayoutNodeDescription node, HttpServletRequest request, Map<String, Map<String, String>> attributes) { // Attempt to set the object attributes for (String name : attributes.get("attributes").keySet()) { try { BeanUtils.setProperty(node, name, attributes.get(name)); } catch (IllegalAccessException | InvocationTargetException e) { logger.warn( "Unable to set attribute: " + name + "on object of type: " + node.getType()); } } // Set the structure-attributes, whatever they may be Map<String, String> structureAttributes = attributes.get("structureAttributes"); if (structureAttributes != null) { for (String name : structureAttributes.keySet()) { this.stylesheetUserPreferencesService.setLayoutAttribute( request, PreferencesScope.STRUCTURE, node.getId(), name, structureAttributes.get(name)); } } } /** * Update the attributes for the node. Unrecognized attributes will log a warning, but are * otherwise ignored. * * @param request * @param response * @param targetId - the id of the node whose attributes will be updated. * @param attributes - parse the JSON name-value pairs in the body as the attributes of the * folder. e.g. : { "structureAttributes" : {"display" : "row", "other" : "another" }, * "attributes" : {"hidden": "true", "type" : "header-top" } } */ @RequestMapping(method = RequestMethod.POST, params = "action=updateAttributes") public ModelAndView updateAttributes( HttpServletRequest request, HttpServletResponse response, @RequestParam("targetId") String targetId, @RequestBody Map<String, Map<String, String>> attributes) { IUserLayoutManager ulm = userInstanceManager .getUserInstance(request) .getPreferencesManager() .getUserLayoutManager(); if (!ulm.getNode(targetId).isEditAllowed()) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "error", getMessage( "error.element.update", "Unable to update element", RequestContextUtils.getLocale(request)))); } // Update the attributes based on the supplied JSON (request body name-value pairs) IUserLayoutNodeDescription node = ulm.getNode(targetId); if (node == null) { logger.warn("[updateAttributes()] Unable to locate node with id: " + targetId); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView( "jsonView", Collections.singletonMap( "error", "Unable to locate node with id: " + targetId)); } else { setObjectAttributes(node, request, attributes); final Locale locale = RequestContextUtils.getLocale(request); try { ulm.saveUserLayout(); } catch (PortalException e) { return handlePersistError(request, response, e); } Map<String, String> model = Collections.singletonMap( "success", getMessage( "success.element.update", "Updated element attributes", locale)); return new ModelAndView("jsonView", model); } } /** * Rename a specified tab. * * @param request * @throws IOException */ @RequestMapping(method = RequestMethod.POST, params = "action=renameTab") public ModelAndView renameTab(HttpServletRequest request, HttpServletResponse response) throws IOException { IUserInstance ui = userInstanceManager.getUserInstance(request); UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); // element ID of the tab to be renamed String tabId = request.getParameter("tabId"); IUserLayoutFolderDescription tab = (IUserLayoutFolderDescription) ulm.getNode(tabId); // desired new name String tabName = request.getParameter("tabName"); if (!ulm.canUpdateNode(tab)) { logger.warn("Attempting to rename an immutable tab"); response.sendError(HttpServletResponse.SC_FORBIDDEN); return new ModelAndView( "jsonView", Collections.singletonMap( "error", getMessage( "error.element.update", "Unable to update element", RequestContextUtils.getLocale(request)))); } /* * Update the tab and save the layout */ tab.setName(StringUtils.isBlank(tabName) ? DEFAULT_TAB_NAME : tabName); final boolean updated = ulm.updateNode(tab); if (updated) { try { // save the user's layout ulm.saveUserLayout(); } catch (PortalException e) { return handlePersistError(request, response, e); } //TODO why do we have to do this, shouldn't modifying the layout be enough to trigger a full re-render (layout's cache key changes) this.stylesheetUserPreferencesService.setLayoutAttribute( request, PreferencesScope.STRUCTURE, tabId, "name", tabName); } Map<String, String> model = Collections.singletonMap("message", "saved new tab name"); return new ModelAndView("jsonView", model); } @RequestMapping(method = RequestMethod.POST, params = "action=updatePermissions") public ModelAndView updatePermissions(HttpServletRequest request, HttpServletResponse response) throws IOException { IUserInstance ui = userInstanceManager.getUserInstance(request); UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); String elementId = request.getParameter("elementID"); IUserLayoutNodeDescription node = ulm.getNode(elementId); if (node == null) { logger.warn("Failed to locate node for permissions update"); response.sendError(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView( "jsonView", Collections.singletonMap("error", "Invalid node id " + elementId)); } String deletable = request.getParameter("deletable"); if (!StringUtils.isBlank(deletable)) { node.setDeleteAllowed(Boolean.valueOf(deletable)); } String movable = request.getParameter("movable"); if (!StringUtils.isBlank(movable)) { node.setMoveAllowed(Boolean.valueOf(movable)); } String editable = request.getParameter("editable"); if (!StringUtils.isBlank(editable)) { node.setEditAllowed(Boolean.valueOf(editable)); } String canAddChildren = request.getParameter("addChildAllowed"); if (!StringUtils.isBlank(canAddChildren)) { node.setAddChildAllowed(Boolean.valueOf(canAddChildren)); } ulm.updateNode(node); try { // save the user's layout ulm.saveUserLayout(); } catch (PortalException e) { return handlePersistError(request, response, e); } return new ModelAndView("jsonView", Collections.EMPTY_MAP); } private ModelAndView handlePersistError( HttpServletRequest request, HttpServletResponse response, Exception e) { logger.warn("Error saving layout", e); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return new ModelAndView( "jsonView", Collections.singletonMap( "error", getMessage( "error.persisting.attribute.change", "Unable to save attribute changes", RequestContextUtils.getLocale(request)))); } protected void removeSubscription(IPerson per, String elementId, IUserLayoutManager ulm) { // get the fragment owner's ID from the element string String userIdString = StringUtils.substringBetween( elementId, Constants.FRAGMENT_ID_USER_PREFIX, Constants.FRAGMENT_ID_LAYOUT_PREFIX); int userId = NumberUtils.toInt(userIdString, 0); // construct a new person object representing the fragment owner RestrictedPerson fragmentOwner = PersonFactory.createRestrictedPerson(); fragmentOwner.setID(userId); fragmentOwner.setUserName(userIdentityStore.getPortalUserName(userId)); // attempt to find a subscription for this fragment IUserFragmentSubscription subscription = userFragmentInfoDao.getUserFragmentInfo(per, fragmentOwner); // if a subscription was found, remove it's registration if (subscription != null) { userFragmentInfoDao.deleteUserFragmentInfo(subscription); ulm.loadUserLayout(true); } // otherwise, delete the node else { ulm.deleteNode(elementId); } } /** * A folder is a tab if its parent element is the layout element * * @param ulm User Layout Manager * @param folderId the folder in question * @return <code>true</code> if the folder is a tab, otherwise <code>false</code> */ protected boolean isTab(IUserLayoutManager ulm, String folderId) throws PortalException { // we could be a bit more careful here and actually check the type return ulm.getRootFolderId().equals(ulm.getParentId(folderId)); } protected IPerson getPerson(IUserInstance ui, HttpServletResponse response) throws IOException { IPerson per = ui.getPerson(); if (per.isGuest()) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED); return null; } return per; } /** * Syntactic sugar for safely resolving a no-args message from message bundle. * * @param key Message bundle key * @param defaultMessage Ready-to-present message to fall back upon. * @param locale desired locale * @return Resolved interpolated message or defaultMessage. */ protected String getMessage(String key, String defaultMessage, Locale locale) { try { return messageSource.getMessage(key, new Object[] {}, defaultMessage, locale); } catch (Exception e) { // sadly, messageSource.getMessage can throw e.g. when message is ill formatted. logger.error("Error resolving message with key {}.", key, e); return defaultMessage; } } /** * Syntactic sugar for safely resolving a one-arg message from message bundle. * * @param key Message bundle key * @param argument dynamic value to be interpolated * @param defaultMessage Ready-to-present message to fall back upon. * @param locale desired locale * @return Resolved interpolated message or defaultMessage. */ protected String getMessage(String key, String argument, String defaultMessage, Locale locale) { try { return messageSource.getMessage(key, new String[] {argument}, defaultMessage, locale); } catch (Exception e) { // sadly, messageSource.getMessage can throw e.g. when message is ill formatted. logger.error("Error resolving message with key {}.", key, e); return defaultMessage; } } protected IAuthorizationPrincipal getUserPrincipal(final String userName) { final IEntity user = GroupService.getEntity(userName, IPerson.class); if (user == null) { return null; } final AuthorizationService authService = AuthorizationService.instance(); return authService.newPrincipal(user); } protected String getTabIdFromName(IUserLayout userLayout, String tabName) { @SuppressWarnings("unchecked") Enumeration<String> childrenOfRoot = userLayout.getChildIds(userLayout.getRootId()); while (childrenOfRoot .hasMoreElements()) { //loop over folders that might be the favorites folder String nodeId = childrenOfRoot.nextElement(); try { IUserLayoutNodeDescription nodeDescription = userLayout.getNodeDescription(nodeId); IUserLayoutNodeDescription.LayoutNodeType nodeType = nodeDescription.getType(); if (FOLDER.equals(nodeType) && nodeDescription instanceof IUserLayoutFolderDescription) { IUserLayoutFolderDescription folderDescription = (IUserLayoutFolderDescription) nodeDescription; if (tabName.equalsIgnoreCase(folderDescription.getName())) { return folderDescription.getId(); } } } catch (Exception e) { logger.error("Error getting the nodeID of the tab name " + tabName, e); } } logger.warn("Tab " + tabName + " was searched for but not found"); return null; //didn't find tab } /** * Moves the source element. * * <p>- If the destination is a tab, the new element automatically goes to the end of the first * column or in a new column. - If the destination is a folder, the element is added to the end * of the folder. - Otherwise, the element is inserted before the destination (the destination * can't be a tab or folder so it must be a portlet). * * @return true if the element was moved and saved. */ private boolean moveElementInternal( HttpServletRequest request, String sourceId, String destinationId, String method) { logger.debug( "moveElementInternal invoked for sourceId={}, destinationId={}, method={}", sourceId, destinationId, method); if (StringUtils.isEmpty(destinationId)) { //shortcut for beginning and end return true; } IUserInstance ui = userInstanceManager.getUserInstance(request); UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); boolean success = false; if (isTab(ulm, destinationId)) { // If the target is a tab type node, move the element to the end of the first column. // TODO Try to insert it into the first available column if multiple columns Enumeration<String> columns = ulm.getChildIds(destinationId); if (columns.hasMoreElements()) { success = attemptNodeMove(ulm, sourceId, columns.nextElement(), null); } else { // Attempt to create a new column IUserLayoutFolderDescription newColumn = new UserLayoutFolderDescription(); newColumn.setName("Column"); newColumn.setId("tbd"); newColumn.setFolderType(IUserLayoutFolderDescription.REGULAR_TYPE); newColumn.setHidden(false); newColumn.setUnremovable(false); newColumn.setImmutable(false); // add the column to our layout IUserLayoutNodeDescription col = ulm.addNode(newColumn, destinationId, null); // If column was created (might not if the tab had addChild=false), move the channel. if (col != null) { success = attemptNodeMove(ulm, sourceId, col.getId(), null); } else { logger.info( "Unable to move item into existing columns on tab {} and unable to create new column", destinationId); } } } else { // If destination is a column, attempt to move into end of column if (isFolder(ulm, destinationId)) { success = attemptNodeMove(ulm, sourceId, destinationId, null); } else { // If insertBefore move to prior to node else to end of folder containing node success = attemptNodeMove( ulm, sourceId, ulm.getParentId(destinationId), "insertBefore".equals(method) ? destinationId : null); } } try { if (success) { ulm.saveUserLayout(); } } catch (PortalException e) { logger.warn("Error saving layout", e); return false; } return success; } private boolean attemptNodeMove( IUserLayoutManager ulm, String sourceId, String destinationId, String beforeNode) { boolean success = ulm.moveNode(sourceId, destinationId, beforeNode); if (!success) { logger.warn( "moveNode returned false for sourceId={}, destinationId={}, method={}; " + "Aborting node movement", sourceId, destinationId, beforeNode); } return success; } private boolean isFolder(IUserLayoutManager ulm, String id) { return ulm.getNode(id).getType().equals(IUserLayoutNodeDescription.LayoutNodeType.FOLDER); } }