/** * 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.portlets.marketplace; import static java.lang.String.format; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import javax.portlet.PortletPreferences; import javax.portlet.PortletRequest; import javax.portlet.RenderRequest; import javax.portlet.RenderResponse; import javax.portlet.ResourceRequest; import javax.portlet.ResourceResponse; import javax.servlet.http.HttpServletRequest; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import net.sf.ehcache.Cache; import org.apache.commons.lang.Validate; import org.apereo.portal.EntityIdentifier; import org.apereo.portal.UserPreferencesManager; import org.apereo.portal.groups.IGroupConstants; import org.apereo.portal.layout.IUserLayout; import org.apereo.portal.layout.IUserLayoutManager; import org.apereo.portal.layout.IUserLayoutStore; import org.apereo.portal.layout.dlm.DistributedUserLayout; import org.apereo.portal.layout.node.IUserLayoutNodeDescription; import org.apereo.portal.layout.node.UserLayoutChannelDescription; import org.apereo.portal.portlet.dao.IMarketplaceRatingDao; import org.apereo.portal.portlet.dao.IPortletDefinitionDao; import org.apereo.portal.portlet.marketplace.IMarketplaceRating; import org.apereo.portal.portlet.marketplace.IMarketplaceService; import org.apereo.portal.portlet.marketplace.MarketplacePortletDefinition; import org.apereo.portal.portlet.om.IPortletDefinition; import org.apereo.portal.portlet.om.PortletCategory; import org.apereo.portal.portlet.registry.IPortletCategoryRegistry; import org.apereo.portal.portlet.registry.IPortletDefinitionRegistry; import org.apereo.portal.portlets.favorites.FavoritesUtils; import org.apereo.portal.rest.layout.MarketplaceEntry; import org.apereo.portal.security.AuthorizationPrincipalHelper; import org.apereo.portal.security.IAuthorizationPrincipal; import org.apereo.portal.security.IPermission; import org.apereo.portal.security.IPerson; import org.apereo.portal.security.IPersonManager; import org.apereo.portal.security.PermissionHelper; import org.apereo.portal.services.GroupService; import org.apereo.portal.url.IPortalRequestUtils; 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.Qualifier; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.context.request.WebRequest; import org.springframework.web.portlet.bind.annotation.RenderMapping; import org.springframework.web.portlet.bind.annotation.ResourceMapping; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @Controller @RequestMapping("VIEW") public class PortletMarketplaceController { protected final Logger logger = LoggerFactory.getLogger(getClass()); private static String SHOW_ROOT_CATEGORY_PREFERENCE = "showRootCategory"; /** * Optional, multi-valued preference that (if specified) limits the portlets displayed in the * Marketplace to those belonging to one or more of these categories. * * @since 4.3 */ private static String PERMITTED_CATEGORIES_PREFERENCE = "permittedCategories"; private static String ENABLE_REVIEWS_PREFERENCE = "PortletMarketplaceController.enableReviews"; private static String ENABLE_REVIEWS_DEFAULT = "true"; /** * Caches objects related to the ability to limit the portlets displayed in a single publication * of the Marketplace. */ @Autowired @Qualifier( value = "org.apereo.portal.portlet.marketplace.MarketplaceService.marketplaceCategoryCache" ) private Cache marketplaceCategoryCache; private IMarketplaceService marketplaceService; private IPortalRequestUtils portalRequestUtils; private IPortletDefinitionRegistry portletDefinitionRegistry; private IPersonManager personManager; private IPortletCategoryRegistry portletCategoryRegistry; private IPortletDefinitionDao portletDefinitionDao; private IMarketplaceRatingDao marketplaceRatingDAO; private IUserInstanceManager userInstanceManager; private IUserLayoutStore userLayoutStore; @Autowired public void setMarketplaceService(IMarketplaceService marketplaceService) { this.marketplaceService = marketplaceService; } @Autowired public void setPortletDefinitionDao(IPortletDefinitionDao portletDefinitionDao) { this.portletDefinitionDao = portletDefinitionDao; } @Autowired public void setMarketplaceRatingDAO(IMarketplaceRatingDao marketplaceRatingDAO) { this.marketplaceRatingDAO = marketplaceRatingDAO; } @Autowired public void setPortletCategoryRegistry(IPortletCategoryRegistry portletCategoryRegistry) { this.portletCategoryRegistry = portletCategoryRegistry; } @Autowired public void setPersonManager(IPersonManager personManager) { this.personManager = personManager; } @Autowired public void setPortletDefinitionRegistry(IPortletDefinitionRegistry portletDefinitionRegistry) { this.portletDefinitionRegistry = portletDefinitionRegistry; } @Autowired public void setPortalRequestUtils(IPortalRequestUtils portalRequestUtils) { this.portalRequestUtils = portalRequestUtils; } @Autowired public void setUserInstanceManager(final IUserInstanceManager userInstanceManager) { this.userInstanceManager = userInstanceManager; } @Autowired public void setUserLayoutStore(final IUserLayoutStore userLayoutStore) { this.userLayoutStore = userLayoutStore; } /** * Returns a view of the marketplace landing page * * @param webRequest * @param portletRequest * @param model * @param initialFilter - optional request paramter. Use to init filter on initial view * @return a string representing the initial view. */ @RenderMapping public String initializeView( WebRequest webRequest, PortletRequest portletRequest, Model model, @RequestParam(required = false) String initialFilter) { this.setUpInitialView(webRequest, portletRequest, model, initialFilter); return "jsp/Marketplace/portlet/view"; } @RenderMapping(params = "action=view") public String entryView( RenderRequest renderRequest, RenderResponse renderResponse, WebRequest webRequest, PortletRequest portletRequest, Model model) { IPortletDefinition result = this.portletDefinitionRegistry.getPortletDefinitionByFname( portletRequest.getParameter("fName")); if (result == null) { this.setUpInitialView(webRequest, portletRequest, model, null); return "jsp/Marketplace/portlet/view"; } final HttpServletRequest servletRequest = this.portalRequestUtils.getPortletHttpRequest(portletRequest); final IPerson user = personManager.getPerson(servletRequest); final IAuthorizationPrincipal principal = AuthorizationPrincipalHelper.principalFromUser(user); if (!this.marketplaceService.mayBrowsePortlet(principal, result)) { // TODO: provide an error experience // currently at least blocks rendering the entry for the portlet the user is not authorized to see. this.setUpInitialView(webRequest, portletRequest, model, null); return "jsp/Marketplace/portlet/view"; } MarketplacePortletDefinition mpDefinition = marketplaceService.getOrCreateMarketplacePortletDefinition(result); IMarketplaceRating tempRatingImpl = marketplaceRatingDAO.getRating( portletRequest.getRemoteUser(), portletDefinitionDao.getPortletDefinitionByFname(result.getFName())); final MarketplaceEntry marketplaceEntry = new MarketplaceEntry(mpDefinition, user); model.addAttribute("marketplaceRating", tempRatingImpl); model.addAttribute("reviewMaxLength", IMarketplaceRating.REVIEW_MAX_LENGTH); model.addAttribute("marketplaceEntry", marketplaceEntry); model.addAttribute("shortURL", mpDefinition.getShortURL()); // User allowed to favorite this portlet? final String targetString = PermissionHelper.permissionTargetIdForPortletDefinition(mpDefinition); final boolean canFavorite = principal.hasPermission( IPermission.PORTAL_SYSTEM, IPermission.PORTLET_FAVORITE_ACTIVITY, targetString); model.addAttribute("canFavorite", canFavorite); // Reviews feature enabled? final PortletPreferences prefs = renderRequest.getPreferences(); final String enableReviewsPreferenceValue = prefs.getValue(ENABLE_REVIEWS_PREFERENCE, ENABLE_REVIEWS_DEFAULT); model.addAttribute("enableReviews", Boolean.valueOf(enableReviewsPreferenceValue)); return "jsp/Marketplace/portlet/entry"; } /** * Use to save the rating of portlet * * @param request * @param response * @param portletFName fname of the portlet to rate * @param rating will be parsed to int * @param review optional review to be saved along with rating * @throws NumberFormatException if rating cannot be parsed to an int */ @ResourceMapping("saveRating") public void saveRating( ResourceRequest request, ResourceResponse response, PortletRequest portletRequest, @RequestParam String portletFName, @RequestParam String rating, @RequestParam(required = false) String review) { Validate.notNull(rating, "Please supply a rating - should not be null"); Validate.notNull(portletFName, "Please supply a portlet to rate - should not be null"); // Make certain reviews are permitted before trying to save one final PortletPreferences prefs = request.getPreferences(); final String enableReviewsPreferenceValue = prefs.getValue(ENABLE_REVIEWS_PREFERENCE, ENABLE_REVIEWS_DEFAULT); if (!Boolean.valueOf(enableReviewsPreferenceValue)) { // Clear the parameter if sent... review = null; } marketplaceRatingDAO.createOrUpdateRating( Integer.parseInt(rating), portletRequest.getRemoteUser(), review, portletDefinitionDao.getPortletDefinitionByFname(portletFName)); } /** * @param request * @param response * @param portletRequest * @return 'rating' as a JSON object. Can be null if rating doesn't exist. */ @ResourceMapping("getRating") public String getRating( ResourceRequest request, ResourceResponse response, @RequestParam String portletFName, PortletRequest portletRequest, Model model) { Validate.notNull( portletFName, "Please supply a portlet to get rating for - should not be null"); IMarketplaceRating tempRating = marketplaceRatingDAO.getRating( portletRequest.getRemoteUser(), portletDefinitionDao.getPortletDefinitionByFname(portletFName)); model.addAttribute("rating", tempRating == null ? null : tempRating.getRating()); return "json"; } @ResourceMapping("layoutInfo") public String getLayoutInfo( ResourceRequest request, @RequestParam String portletFName, Model model) throws TransformerException { Validate.notNull(portletFName, "Please supply a portlet fname"); final HttpServletRequest servletRequest = this.portalRequestUtils.getPortletHttpRequest(request); IUserInstance ui = userInstanceManager.getUserInstance(servletRequest); UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); IUserLayoutManager ulm = upm.getUserLayoutManager(); IPerson person = ui.getPerson(); DistributedUserLayout userLayout = userLayoutStore.getUserLayout(person, upm.getUserProfile()); List<PortletTab> tabs = getPortletTabInfo(userLayout, portletFName); boolean isFavorite = isPortletFavorited(ulm.getUserLayout(), portletFName); model.addAttribute("favorite", isFavorite); model.addAttribute("tabs", tabs); return "json"; } private void setUpInitialView( WebRequest webRequest, PortletRequest portletRequest, Model model, String initialFilter) { // We'll track and potentially log the time it takes to perform this initialization final long timestamp = System.currentTimeMillis(); final HttpServletRequest servletRequest = this.portalRequestUtils.getPortletHttpRequest(portletRequest); final PortletPreferences preferences = portletRequest.getPreferences(); final boolean isLogLevelDebug = logger.isDebugEnabled(); final IPerson user = personManager.getPerson(servletRequest); final Map<String, Set<?>> registry = getRegistry(user, portletRequest); @SuppressWarnings("unchecked") final Set<MarketplaceEntry> marketplaceEntries = (Set<MarketplaceEntry>) registry.get("portlets"); model.addAttribute("marketplaceEntries", marketplaceEntries); @SuppressWarnings("unchecked") Set<PortletCategory> categoryList = (Set<PortletCategory>) registry.get("categories"); @SuppressWarnings("unchecked") final Set<MarketplaceEntry> featuredPortlets = (Set<MarketplaceEntry>) registry.get("featured"); model.addAttribute("featuredEntries", featuredPortlets); //Determine if the marketplace is going to show the root category String showRootCategoryPreferenceValue = preferences.getValue(SHOW_ROOT_CATEGORY_PREFERENCE, "false"); boolean showRootCategory = Boolean.parseBoolean(showRootCategoryPreferenceValue); if (isLogLevelDebug) { logger.debug("Going to show Root Category?: {}", Boolean.toString(showRootCategory)); } if (showRootCategory == false) { categoryList.remove(this.portletCategoryRegistry.getTopLevelPortletCategory()); } model.addAttribute("categoryList", categoryList); model.addAttribute("initialFilter", initialFilter); logger.debug( "Marketplace took {}ms in setUpInitialView for user '{}'", System.currentTimeMillis() - timestamp, user.getUserName()); } /** * Returns a set of MarketplacePortletDefinitions. Supply a user to limit the set to only * portlets the user can use. If user is null, this will return all portlets. Setting user to * null will superscede all other parameters. * * @param user - non-null user to limit results by. This will filter results to only portlets * that user can use. * @return a set of portlets filtered that user can use, and other parameters */ public Map<String, Set<?>> getRegistry(final IPerson user, final PortletRequest req) { Map<String, Set<?>> registry = new TreeMap<String, Set<?>>(); // Empty, or the set of categories that are permitted to // be displayed in the Portlet Marketplace (portlet) final Set<PortletCategory> permittedCategories = getPermittedCategories(req); final Set<MarketplaceEntry> visiblePortlets = this.marketplaceService.browseableMarketplaceEntriesFor(user, permittedCategories); final Set<PortletCategory> visibleCategories = this.marketplaceService.browseableNonEmptyPortletCategoriesFor( user, permittedCategories); final Set<MarketplaceEntry> featuredPortlets = this.marketplaceService.featuredEntriesForUser(user, permittedCategories); registry.put("portlets", visiblePortlets); registry.put("categories", visibleCategories); registry.put("featured", featuredPortlets); return registry; } private boolean isPortletFavorited(IUserLayout layout, String fname) { List<IUserLayoutNodeDescription> favorites = FavoritesUtils.getFavoritePortlets(layout); for (IUserLayoutNodeDescription favorite : favorites) { if (favorite instanceof UserLayoutChannelDescription) { String channelId = ((UserLayoutChannelDescription) favorite).getChannelPublishId(); IPortletDefinition portletDefinition = portletDefinitionRegistry.getPortletDefinition(channelId); String favFName = portletDefinition.getFName(); if (fname != null && fname.equals(favFName)) { return true; } } } return false; } private List<PortletTab> getPortletTabInfo(DistributedUserLayout layout, String fname) { final String XPATH_TAB = "/layout/folder/folder[@hidden = 'false' and @type = 'regular']"; final String XPATH_COUNT_COLUMNS = "count(./folder[@hidden = \"false\"])"; final String XPATH_COUNT_NON_EDITABLE_COLUMNS = "count(./folder[@hidden = \"false\" and @*[local-name() = \"editAllowed\"] = \"false\"])"; final String XPATH_GET_TAB_PORTLET_FMT = ".//channel[@hidden = \"false\" and @fname = \"%s\"]"; Document doc = layout.getLayout(); XPathFactory xpathFactory = XPathFactory.newInstance(); XPath xpath = xpathFactory.newXPath(); try { XPathExpression tabExpr = xpath.compile(XPATH_TAB); NodeList list = (NodeList) tabExpr.evaluate(doc, XPathConstants.NODESET); // Count columns and non-editable columns... XPathExpression columnCountExpr = xpath.compile(XPATH_COUNT_COLUMNS); XPathExpression nonEditableCountExpr = xpath.compile(XPATH_COUNT_NON_EDITABLE_COLUMNS); // get the list of tabs... String xpathStr = format(XPATH_GET_TAB_PORTLET_FMT, fname); XPathExpression portletExpr = xpath.compile(xpathStr); List<PortletTab> tabs = new ArrayList<>(); for (int i = 0; i < list.getLength(); i++) { Node tab = list.item(i); String tabName = ((Element) tab).getAttribute("name"); String tabId = ((Element) tab).getAttribute("ID"); // check if tab is editable... Number columns = (Number) columnCountExpr.evaluate(tab, XPathConstants.NUMBER); Number nonEditColumns = (Number) nonEditableCountExpr.evaluate(tab, XPathConstants.NUMBER); // tab is not editable... skip it... if (columns.intValue() > 0 && columns.intValue() == nonEditColumns.intValue()) { continue; } // get all instances of this portlet on this tab... List<String> layoutIds = new ArrayList<>(); NodeList fnameListPerTab = (NodeList) portletExpr.evaluate(tab, XPathConstants.NODESET); for (int j = 0; j < fnameListPerTab.getLength(); j++) { Node channel = fnameListPerTab.item(j); String layoutId = ((Element) channel).getAttribute("ID"); layoutIds.add(layoutId); } PortletTab tabInfo = new PortletTab(tabName, tabId, layoutIds); tabs.add(tabInfo); } return tabs; } catch (XPathExpressionException e) { logger.error("Error evaluating xpath", e); } return null; } public static final class PortletTab { private final String name; private final String id; private final List<String> layoutIds; public PortletTab(final String name, final String id, final List<String> layoutIds) { this.name = name; this.id = id; this.layoutIds = (layoutIds == null) ? Collections.<String>emptyList() : layoutIds; } public String getName() { return name; } public String getId() { return id; } public List<String> getLayoutIds() { return Collections.unmodifiableList(layoutIds); } } private Set<PortletCategory> getPermittedCategories(PortletRequest req) { Set<PortletCategory> rslt = Collections.emptySet(); // default final PortletPreferences prefs = req.getPreferences(); final String[] permittedCategories = prefs.getValues(PERMITTED_CATEGORIES_PREFERENCE, new String[0]); if (permittedCategories.length != 0) { // Expensive to create, use cache for this collection... Set<String> cacheKey = new HashSet<>(Arrays.asList(permittedCategories)); net.sf.ehcache.Element cacheElement = marketplaceCategoryCache.get(cacheKey); if (cacheElement == null) { // Nothing in cache currently; need to populate cache HashSet<PortletCategory> portletCategories = new HashSet<>(); for (final String categoryName : permittedCategories) { EntityIdentifier[] cats = GroupService.searchForGroups( categoryName, IGroupConstants.IS, IPortletDefinition.class); if (cats != null && cats.length > 0) { PortletCategory pc = portletCategoryRegistry.getPortletCategory(cats[0].getKey()); if (pc != null) { portletCategories.add(pc); } else { logger.warn( "No PortletCategory found in portletCategoryRegistry for id '{}'", cats[0].getKey()); } } else { logger.warn( "No category found in GroupService for name '{}'", categoryName); } } /* * Sanity Check: Since at least 1 category name was specified, we * need to make certain there's at least 1 PortletCategory in the * set; otherwise, a restricted Marketplace portlet would become * an unrestricted one. */ if (portletCategories.isEmpty()) { throw new IllegalStateException( "None of the specified category " + "names could be resolved to a PortletCategory: " + Arrays.asList(permittedCategories)); } cacheElement = new net.sf.ehcache.Element(cacheKey, portletCategories); this.marketplaceCategoryCache.put(cacheElement); } rslt = (Set<PortletCategory>) cacheElement.getObjectValue(); } return rslt; } }