/**
* 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.portlet.marketplace;
import com.google.common.collect.ImmutableSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import org.apache.commons.lang3.Validate;
import org.apereo.portal.concurrency.caching.RequestCache;
import org.apereo.portal.events.LoginEvent;
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.rest.layout.MarketplaceEntry;
import org.apereo.portal.security.AuthorizationPrincipalHelper;
import org.apereo.portal.security.IAuthorizationPrincipal;
import org.apereo.portal.security.IAuthorizationService;
import org.apereo.portal.security.IPermission;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.security.PermissionHelper;
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.beans.factory.annotation.Value;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
/**
* Service layer implementation for Marketplace.
*
* @since 4.1
*/
@Service
public class MarketplaceService implements IMarketplaceService, ApplicationListener<LoginEvent> {
public static String FEATURED_CATEGORY_NAME = "Featured";
protected final Logger logger = LoggerFactory.getLogger(getClass());
private IPortletDefinitionRegistry portletDefinitionRegistry;
private IPortletCategoryRegistry portletCategoryRegistry;
private IAuthorizationService authorizationService;
private boolean enableMarketplacePreloading = false;
@Autowired
public void setAuthorizationService(IAuthorizationService service) {
this.authorizationService = service;
}
/** Used to store individual MarketplacePortletDefinition instances. */
@Autowired
@Qualifier(
value =
"org.apereo.portal.portlet.marketplace.MarketplaceService.marketplacePortletDefinitionCache"
)
private Cache marketplacePortletDefinitionCache;
/** Cache of Username -> Future<Set<MarketplaceEntry> */
@Autowired
@Qualifier(
value =
"org.apereo.portal.portlet.marketplace.MarketplaceService.marketplaceUserPortletDefinitionCache"
)
private Cache marketplaceUserPortletDefinitionCache;
/**
* 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;
@Value("${org.apereo.portal.portlets.marketplacePortlet.loadMarketplaceOnLogin:false}")
public void setLoadMarketplaceOnLogin(final boolean enableMarketplacePreloading) {
this.enableMarketplacePreloading = enableMarketplacePreloading;
}
/**
* Handle the portal LoginEvent. If marketplace caching is enabled, will preload marketplace
* entries for the currently logged in user.
*
* @param loginEvent the login event.
*/
@Override
public void onApplicationEvent(LoginEvent loginEvent) {
if (enableMarketplacePreloading) {
final IPerson person = loginEvent.getPerson();
/*
* Passing an empty collection pre-loads an unfiltered collection;
* instances of PortletMarketplace that specify filtering will
* trigger a new collection to be loaded.
*/
final Set<PortletCategory> empty = Collections.emptySet();
loadMarketplaceEntriesFor(person, empty);
}
}
/**
* Load the list of marketplace entries for a user. Will load entries async. This method is
* primarily intended for seeding data. Most impls should call browseableMarketplaceEntriesFor()
* instead.
*
* <p>Note: Set is immutable since it is potentially shared between threads. If the set needs
* mutability, be sure to consider the thread safety implications. No protections have been
* provided against modifying the MarketplaceEntry itself, so be careful when modifying the
* entities contained in the list.
*
* @param user The non-null user
* @param categories Restricts the output to entries within the specified categories if
* non-empty
* @return a Future that will resolve to a set of MarketplaceEntry objects the requested user
* has browse access to.
* @throws java.lang.IllegalArgumentException if user is null
* @since 4.2
*/
@Async
public Future<ImmutableSet<MarketplaceEntry>> loadMarketplaceEntriesFor(
final IPerson user, final Set<PortletCategory> categories) {
final IAuthorizationPrincipal principal =
AuthorizationPrincipalHelper.principalFromUser(user);
List<IPortletDefinition> allDisplayablePortletDefinitions =
this.portletDefinitionRegistry.getAllPortletDefinitions();
if (!categories.isEmpty()) {
// Indicates we plan to restrict portlets displayed in the Portlet
// Marketplace to those that belong to one or more specified groups.
Element portletDefinitionsElement = marketplaceCategoryCache.get(categories);
if (portletDefinitionsElement == null) {
/*
* Collection not in cache -- need to recreate it
*/
// Gather the complete collection of allowable categories (specified categories & their descendants)
final Set<PortletCategory> allSpecifiedAndDecendantCategories = new HashSet<>();
for (PortletCategory pc : categories) {
collectSpecifiedAndDescendantCategories(pc, allSpecifiedAndDecendantCategories);
}
// Filter portlets that match the criteria
Set<IPortletDefinition> filteredPortletDefinitions = new HashSet<>();
for (final IPortletDefinition portletDefinition :
allDisplayablePortletDefinitions) {
final Set<PortletCategory> parents =
portletCategoryRegistry.getParentCategories(portletDefinition);
for (final PortletCategory parent : parents) {
if (allSpecifiedAndDecendantCategories.contains(parent)) {
filteredPortletDefinitions.add(portletDefinition);
break;
}
}
}
portletDefinitionsElement =
new Element(categories, new ArrayList<>(filteredPortletDefinitions));
marketplaceCategoryCache.put(portletDefinitionsElement);
}
allDisplayablePortletDefinitions =
(List<IPortletDefinition>) portletDefinitionsElement.getObjectValue();
}
final Set<MarketplaceEntry> visiblePortletDefinitions = new HashSet<>();
for (final IPortletDefinition portletDefinition : allDisplayablePortletDefinitions) {
if (mayBrowsePortlet(principal, portletDefinition)) {
final MarketplacePortletDefinition marketplacePortletDefinition =
getOrCreateMarketplacePortletDefinition(portletDefinition);
final MarketplaceEntry entry =
new MarketplaceEntry(marketplacePortletDefinition, user);
// flag whether this use can add the portlet...
boolean canAdd = mayAddPortlet(user, portletDefinition);
entry.setCanAdd(canAdd);
visiblePortletDefinitions.add(entry);
}
}
logger.trace(
"These portlet definitions {} are browseable by {}.",
visiblePortletDefinitions,
user);
Future<ImmutableSet<MarketplaceEntry>> result =
new AsyncResult<>(ImmutableSet.copyOf(visiblePortletDefinitions));
Element cacheElement = new Element(user.getUserName(), result);
marketplaceUserPortletDefinitionCache.put(cacheElement);
return result;
}
@Override
public ImmutableSet<MarketplaceEntry> browseableMarketplaceEntriesFor(
final IPerson user, final Set<PortletCategory> categories) {
Element cacheElement = marketplaceUserPortletDefinitionCache.get(user.getUserName());
Future<ImmutableSet<MarketplaceEntry>> future = null;
if (cacheElement == null) {
// not in cache, load it and cache the results...
future = loadMarketplaceEntriesFor(user, categories);
} else {
future = (Future<ImmutableSet<MarketplaceEntry>>) cacheElement.getObjectValue();
}
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
logger.error(e.getMessage(), e);
return ImmutableSet.of();
}
}
@Override
public Set<PortletCategory> browseableNonEmptyPortletCategoriesFor(
final IPerson user, final Set<PortletCategory> categories) {
final IAuthorizationPrincipal principal =
AuthorizationPrincipalHelper.principalFromUser(user);
final Set<MarketplaceEntry> browseablePortlets =
browseableMarketplaceEntriesFor(user, categories);
final Set<PortletCategory> browseableCategories = new HashSet<PortletCategory>();
// by considering only the parents of portlets browseable by this user,
// categories containing zero browseable portlets are excluded.
for (final MarketplaceEntry entry : browseablePortlets) {
IPortletDefinition portletDefinition = entry.getMarketplacePortletDefinition();
for (final PortletCategory category :
this.portletCategoryRegistry.getParentCategories(portletDefinition)) {
final String categoryId = category.getId();
if (mayBrowse(principal, categoryId)) {
browseableCategories.add(category);
} else {
logger.trace(
"Portlet {} is browseable by {} but it is in category {} "
+ "which is not browseable by that user. "
+ "This may be as intended, "
+ "or it may be that that portlet category ought to be more widely browseable.",
portletDefinition,
user,
category);
}
}
}
logger.trace("These categories {} are browseable by {}.", browseableCategories, user);
return browseableCategories;
}
@Override
public boolean mayBrowsePortlet(
final IAuthorizationPrincipal principal, final IPortletDefinition portletDefinition) {
Validate.notNull(principal, "Cannot determine if null principals can browse portlets.");
Validate.notNull(
portletDefinition,
"Cannot determine whether a user can browse a null portlet definition.");
final String portletPermissionEntityId =
PermissionHelper.permissionTargetIdForPortletDefinition(portletDefinition);
return mayBrowse(principal, portletPermissionEntityId);
}
@Override
public Set<MarketplaceEntry> featuredEntriesForUser(
final IPerson user, final Set<PortletCategory> categories) {
Validate.notNull(user, "Cannot determine relevant featured portlets for null user.");
final Set<MarketplaceEntry> browseablePortlets =
browseableMarketplaceEntriesFor(user, categories);
final Set<MarketplaceEntry> featuredPortlets = new HashSet<>();
for (final MarketplaceEntry entry : browseablePortlets) {
final IPortletDefinition portletDefinition = entry.getMarketplacePortletDefinition();
for (final PortletCategory category :
this.portletCategoryRegistry.getParentCategories(portletDefinition)) {
if (FEATURED_CATEGORY_NAME.equalsIgnoreCase(category.getName())) {
featuredPortlets.add(entry);
}
}
}
return featuredPortlets;
}
@Override
public MarketplacePortletDefinition getOrCreateMarketplacePortletDefinition(
IPortletDefinition portletDefinition) {
Element element = marketplacePortletDefinitionCache.get(portletDefinition.getFName());
if (element == null) {
final MarketplacePortletDefinition mpd =
new MarketplacePortletDefinition(
portletDefinition, this, portletCategoryRegistry);
element = new Element(portletDefinition.getFName(), mpd);
this.marketplacePortletDefinitionCache.put(element);
}
return (MarketplacePortletDefinition) element.getObjectValue();
}
@Override
public MarketplacePortletDefinition getOrCreateMarketplacePortletDefinitionIfTheFnameExists(
String fname) {
IPortletDefinition portletDefinition =
portletDefinitionRegistry.getPortletDefinitionByFname(fname);
if (portletDefinition != null) {
return getOrCreateMarketplacePortletDefinition(portletDefinition);
}
return null;
}
// Private stateless static utility methods below here
/**
* True if the principal has UP_PORTLET_SUBSCRIBE.BROWSE on the target id. The target ID must be
* fully resolved. This method will not e.g. prepend the portlet prefix to target ids that seem
* like they might be portlet IDs.
*
* <p>Implementation note: technically this method is not stateless since asking an
* AuthorizationPrincipal about its permissions has caching side effects in the permissions
* system, but it's stateless as far as this Service is concerned.
*
* @param principal Non-null IAuthorizationPrincipal who might have permission
* @param targetId Non-null identifier of permission target
* @return true if principal has BROWSE permissions, false otherwise
*/
private static boolean mayBrowse(
final IAuthorizationPrincipal principal, final String targetId) {
Validate.notNull(principal, "Cannot determine permissions for a null user.");
Validate.notNull(targetId, "Cannot determine permissions on a null target.");
return (principal.hasPermission(
IPermission.PORTAL_SUBSCRIBE, IPermission.PORTLET_BROWSE_ACTIVITY, targetId));
}
/** Called recursively to gather all specified categories and descendants */
private void collectSpecifiedAndDescendantCategories(
PortletCategory specified, Set<PortletCategory> gathered) {
final Set<PortletCategory> children =
portletCategoryRegistry.getAllChildCategories(specified);
for (PortletCategory child : children) {
collectSpecifiedAndDescendantCategories(child, gathered);
}
gathered.add(specified);
}
/**
* Answers whether the given user may add the portlet to their layout
*
* @param user a non-null IPerson who might be permitted to add
* @param portletDefinition a non-null portlet definition
* @return true if permitted, false otherwise
* @throws IllegalArgumentException if user is null
* @throws IllegalArgumentException if portletDefinition is null
* @since 4.2
*/
@RequestCache
public boolean mayAddPortlet(final IPerson user, final IPortletDefinition portletDefinition) {
Validate.notNull(user, "Cannot determine if null users can browse portlets.");
Validate.notNull(
portletDefinition,
"Cannot determine whether a user can browse a null portlet definition.");
//short-cut for guest user, it will always be false for guest, otherwise evaluate
return user.isGuest()
? false
: authorizationService.canPrincipalSubscribe(
AuthorizationPrincipalHelper.principalFromUser(user),
portletDefinition.getPortletDefinitionId().getStringId());
}
// JavaBean property setters below here.
// getters omitted because no use cases for reading the properties
@Autowired
public void setPortletDefinitionRegistry(
final IPortletDefinitionRegistry portletDefinitionRegistry) {
Validate.notNull(
portletDefinitionRegistry, "Portlet definition registry must not be null.");
this.portletDefinitionRegistry = portletDefinitionRegistry;
}
@Autowired
public void setPortletCategoryRegistry(final IPortletCategoryRegistry portletCategoryRegistry) {
Validate.notNull(portletCategoryRegistry);
this.portletCategoryRegistry = portletCategoryRegistry;
}
}