org.apereo.portal.layout.dlm.remoting.ChannelListController.java Source code

Java tutorial

Introduction

Here is the source code for org.apereo.portal.layout.dlm.remoting.ChannelListController.java

Source

/**
 * 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 java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.apereo.portal.EntityIdentifier;
import org.apereo.portal.UserPreferencesManager;
import org.apereo.portal.i18n.ILocaleStore;
import org.apereo.portal.i18n.LocaleManager;
import org.apereo.portal.i18n.LocaleManagerFactory;
import org.apereo.portal.layout.IUserLayout;
import org.apereo.portal.layout.IUserLayoutManager;
import org.apereo.portal.layout.dlm.remoting.registry.ChannelBean;
import org.apereo.portal.layout.dlm.remoting.registry.ChannelCategoryBean;
import org.apereo.portal.layout.dlm.remoting.registry.v43.PortletCategoryBean;
import org.apereo.portal.layout.dlm.remoting.registry.v43.PortletDefinitionBean;
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.IPortletDefinitionParameter;
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.security.IAuthorizationPrincipal;
import org.apereo.portal.security.IAuthorizationService;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.security.IPersonManager;
import org.apereo.portal.services.AuthorizationServiceFacade;
import org.apereo.portal.spring.spel.IPortalSpELService;
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.context.MessageSource;
import org.springframework.stereotype.Controller;
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.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;

/**
 * A Spring controller that returns a JSON representation of portlets the user may access in the
 * portal.
 *
 * <p>As of uPortal 4.2, this will return the portlets the user is allowed to browse, regardless
 * whether the portlet has a category (previously it returned portlets the user could subscribe to
 * and left out portlets with no categories but this change makes this API in sync with search and
 * the marketplace and uses the BROWSE permission properly without overloading the meaning of
 * categories).
 */
@Controller
public class ChannelListController {

    private static final String CATEGORIES_MAP_KEY = "categories";
    private static final String UNCATEGORIZED = "uncategorized";
    private static final String UNCATEGORIZED_DESC = "uncategorized.description";
    private static final String ICON_URL_PARAMETER_NAME = "iconUrl";

    /** Moved to PortletRESTController under /api/portlets.json */
    private static final String TYPE_MANAGE = "manage";

    private IPortletDefinitionRegistry portletDefinitionRegistry;
    private IPortletCategoryRegistry portletCategoryRegistry;
    private IPersonManager personManager;
    private IPortalSpELService spELService;
    private ILocaleStore localeStore;
    private LocaleManagerFactory localeManagerFactory;
    private MessageSource messageSource;
    private IAuthorizationService authorizationService;
    private IUserInstanceManager userInstanceManager;
    private FavoritesUtils favoritesUtils;

    @Autowired
    private IMarketplaceService marketplaceService;

    private Logger logger = LoggerFactory.getLogger(getClass());

    /** @param portletDefinitionRegistry The portlet registry bean */
    @Autowired
    public void setPortletDefinitionRegistry(IPortletDefinitionRegistry portletDefinitionRegistry) {
        this.portletDefinitionRegistry = portletDefinitionRegistry;
    }

    @Autowired
    public void setPortletCategoryRegistry(IPortletCategoryRegistry portletCategoryRegistry) {
        this.portletCategoryRegistry = portletCategoryRegistry;
    }

    /**
     * For injection of the person manager. Used for authorization.
     *
     * @param personManager IPersonManager instance
     */
    @Autowired
    public void setPersonManager(IPersonManager personManager) {
        this.personManager = personManager;
    }

    @Autowired
    public void setPortalSpELProvider(IPortalSpELService spELProvider) {
        this.spELService = spELProvider;
    }

    @Autowired
    public void setLocaleStore(ILocaleStore localeStore) {
        this.localeStore = localeStore;
    }

    @Autowired
    public void setLocaleManagerFactory(LocaleManagerFactory localeManagerFactory) {
        this.localeManagerFactory = localeManagerFactory;
    }

    @Autowired
    public void setMessageSource(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @Autowired
    public void setAuthorizationService(IAuthorizationService authorizationService) {
        this.authorizationService = authorizationService;
    }

    @Autowired
    public void setUserInstanceManager(IUserInstanceManager userInstanceManager) {
        this.userInstanceManager = userInstanceManager;
    }

    @Autowired
    public void setFavoritesUtils(FavoritesUtils favoritesUtils) {
        this.favoritesUtils = favoritesUtils;
    }

    /**
     * Original, pre-4.3 version of this API. Always returns the entire contents of the Portlet
     * Registry, including uncategorized portlets, to which the user has access. Access is based on
     * the SUBSCRIBE permission.
     */
    @RequestMapping(value = "/portletList", method = RequestMethod.GET)
    public ModelAndView listChannels(WebRequest webRequest, HttpServletRequest request,
            @RequestParam(value = "type", required = false) String type) {

        if (TYPE_MANAGE.equals(type)) {
            throw new UnsupportedOperationException("Moved to PortletRESTController under /api/portlets.json");
        }

        final IPerson user = personManager.getPerson(request);
        final Map<String, SortedSet<?>> registry = getRegistryOriginal(webRequest, user);

        // Since type=manage was deprecated channels is always empty but retained for backwards
        // compatibility
        registry.put("channels", new TreeSet<ChannelBean>());

        return new ModelAndView("jsonView", "registry", registry);
    }

    /**
     * Updated version of this API. Supports an optional 'categoryId' parameter. If provided, this
     * URL will return the portlet registry beginning with the specified category, including all
     * descendants, and <em>excluding</em> uncategorized portlets. If no 'categoryId' is provided,
     * this method returns the portlet registry beginning with 'All Categories' (the root) and
     * <em>including</em> uncategorized portlets. Access is based on the SUBSCRIBE permission.
     *
     * @since 4.3
     */
    @RequestMapping(value = "/v4-3/dlm/portletRegistry.json", method = RequestMethod.GET)
    public ModelAndView getPortletRegistry(WebRequest webRequest, HttpServletRequest request,
            @RequestParam(value = "categoryId", required = false) String categoryId,
            @RequestParam(value = "category", required = false) String categoryName,
            @RequestParam(value = "favorite", required = false) String favorite) {

        boolean includeUncategorizedPortlets = true; // default

        /*
         * Pick a category for the basis of the response
         */
        PortletCategory rootCategory = portletCategoryRegistry.getTopLevelPortletCategory(); // default
        if (StringUtils.isNotBlank(categoryId)) {
            // Callers can specify a category by Id...
            rootCategory = portletCategoryRegistry.getPortletCategory(categoryId);
            includeUncategorizedPortlets = false;
        } else if (StringUtils.isNotBlank(categoryName)) {
            // or by name...
            rootCategory = portletCategoryRegistry.getPortletCategoryByName(categoryName);
            includeUncategorizedPortlets = false;
        }

        if (rootCategory == null) {
            // A specific category was requested, but there was a problem obtaining it
            throw new IllegalArgumentException("Requested category not found");
        }

        final IPerson user = personManager.getPerson(request);

        /*
         * Gather the user's favorites
         */
        final Set<IPortletDefinition> favoritePortlets = calculateFavoritePortlets(request);

        Map<String, SortedSet<PortletCategoryBean>> rslt = getRegistry43(webRequest, user, rootCategory,
                includeUncategorizedPortlets, favoritePortlets);

        /*
         * The 'favorite=true' option means return only portlets that this user has favorited.
         */
        if (Boolean.valueOf(favorite)) {
            logger.debug(
                    "Filtering out non-favorite portlets because 'favorite=true' was included in the query string");
            rslt = filterRegistryFavoritesOnly(rslt);
        }

        return new ModelAndView("jsonView", "registry", rslt);
    }

    /*
     * Private methods that support the original (pre-4.3) version of the API
     */

    /**
     * Gathers and organizes the response based on the specified rootCategory and the permissions of
     * the specified user.
     */
    private Map<String, SortedSet<?>> getRegistryOriginal(WebRequest request, IPerson user) {

        /*
         * This collection of all the portlets in the portal is for the sake of
         * tracking which ones are uncategorized.
         */
        Set<IPortletDefinition> portletsNotYetCategorized = new HashSet<>(
                portletDefinitionRegistry.getAllPortletDefinitions());

        // construct a new channel registry
        Map<String, SortedSet<?>> rslt = new TreeMap<>();
        SortedSet<ChannelCategoryBean> categories = new TreeSet<>();

        // add the root category and all its children to the registry
        final PortletCategory rootCategory = portletCategoryRegistry.getTopLevelPortletCategory();
        final Locale locale = getUserLocale(user);
        categories.add(prepareCategoryBean(request, rootCategory, portletsNotYetCategorized, user, locale));

        /*
         * uPortal historically has provided for a convention that portlets not in any category
         * may potentially be viewed by users but may not be subscribed to.
         *
         * As of uPortal 4.2, the logic below now takes any portlets the user has BROWSE access to
         * that have not already been identified as belonging to a category and adds them to a category
         * called Uncategorized.
         */

        EntityIdentifier ei = user.getEntityIdentifier();
        IAuthorizationPrincipal ap = AuthorizationServiceFacade.instance().newPrincipal(ei.getKey(), ei.getType());

        // construct a new channel category bean for this category
        String uncategorizedString = messageSource.getMessage(UNCATEGORIZED, new Object[] {}, locale);
        ChannelCategoryBean uncategorizedPortletsBean = new ChannelCategoryBean(
                new PortletCategory(uncategorizedString));
        uncategorizedPortletsBean.setName(UNCATEGORIZED);
        uncategorizedPortletsBean
                .setDescription(messageSource.getMessage(UNCATEGORIZED_DESC, new Object[] {}, locale));

        for (IPortletDefinition portlet : portletsNotYetCategorized) {
            if (authorizationService.canPrincipalBrowse(ap, portlet)) {
                // construct a new channel bean from this channel
                ChannelBean channel = getChannel(portlet, request, locale);
                uncategorizedPortletsBean.addChannel(channel);
            }
        }
        // Add even if no portlets in category
        categories.add(uncategorizedPortletsBean);

        rslt.put(CATEGORIES_MAP_KEY, categories);
        return rslt;
    }

    private ChannelCategoryBean prepareCategoryBean(WebRequest request, PortletCategory category,
            Set<IPortletDefinition> portletsNotYetCategorized, IPerson user, Locale locale) {

        // construct a new channel category bean for this category
        ChannelCategoryBean categoryBean = new ChannelCategoryBean(category);
        categoryBean.setName(messageSource.getMessage(category.getName(), new Object[] {}, locale));

        // add the direct child channels for this category
        Set<IPortletDefinition> portlets = portletCategoryRegistry.getChildPortlets(category);
        EntityIdentifier ei = user.getEntityIdentifier();
        IAuthorizationPrincipal ap = AuthorizationServiceFacade.instance().newPrincipal(ei.getKey(), ei.getType());

        for (IPortletDefinition portlet : portlets) {

            if (authorizationService.canPrincipalBrowse(ap, portlet)) {
                // construct a new channel bean from this channel
                ChannelBean channel = getChannel(portlet, request, locale);
                categoryBean.addChannel(channel);
            }

            /*
             * Remove the portlet from the uncategorized collection;
             * note -- this approach will not prevent portlets from
             * appearing in multiple categories (as appropriate).
             */
            portletsNotYetCategorized.remove(portlet);
        }

        /* Now add child categories. */
        for (PortletCategory childCategory : this.portletCategoryRegistry.getChildCategories(category)) {
            ChannelCategoryBean childCategoryBean = prepareCategoryBean(request, childCategory,
                    portletsNotYetCategorized, user, locale);
            categoryBean.addCategory(childCategoryBean);
        }

        return categoryBean;
    }

    private ChannelBean getChannel(IPortletDefinition definition, WebRequest request, Locale locale) {
        ChannelBean channel = new ChannelBean();
        channel.setId(definition.getPortletDefinitionId().getStringId());
        channel.setDescription(definition.getDescription(locale.toString()));
        channel.setFname(definition.getFName());
        channel.setName(definition.getName(locale.toString()));
        channel.setState(definition.getLifecycleState().toString());
        channel.setTitle(definition.getTitle(locale.toString()));
        channel.setTypeId(definition.getType().getId());

        // See api docs for postProcessIconUrlParameter() below
        IPortletDefinitionParameter iconParameter = definition.getParameter(ICON_URL_PARAMETER_NAME);
        if (iconParameter != null) {
            IPortletDefinitionParameter evaluated = postProcessIconUrlParameter(iconParameter, request);
            channel.setIconUrl(evaluated.getValue());
        }

        return channel;
    }

    /*
     * Private methods that support the 4.3 version of the API
     */

    /**
     * Gathers and organizes the response based on the specified rootCategory and the permissions of
     * the specified user.
     */
    private Map<String, SortedSet<PortletCategoryBean>> getRegistry43(WebRequest request, IPerson user,
            PortletCategory rootCategory, boolean includeUncategorized, Set<IPortletDefinition> favorites) {

        /*
         * This collection of all the portlets in the portal is for the sake of
         * tracking which ones are uncategorized.  They will be added to the
         * output if includeUncategorized=true.
         */
        Set<IPortletDefinition> portletsNotYetCategorized = includeUncategorized
                ? new HashSet<>(portletDefinitionRegistry.getAllPortletDefinitions())
                : new HashSet<>(); // Not necessary to fetch them if we're not
        // tracking them

        // construct a new channel registry
        Map<String, SortedSet<PortletCategoryBean>> rslt = new TreeMap<>();
        SortedSet<PortletCategoryBean> categories = new TreeSet<>();

        // add the root category and all its children to the registry
        final Locale locale = getUserLocale(user);
        categories.add(preparePortletCategoryBean(request, rootCategory, portletsNotYetCategorized, user, locale,
                favorites));

        if (includeUncategorized) {
            /*
             * uPortal historically has provided for a convention that portlets not in any category
             * may potentially be viewed by users but may not be subscribed to.
             *
             * As of uPortal 4.2, the logic below now takes any portlets the user has BROWSE access to
             * that have not already been identified as belonging to a category and adds them to a category
             * called Uncategorized.
             */

            EntityIdentifier ei = user.getEntityIdentifier();
            IAuthorizationPrincipal ap = AuthorizationServiceFacade.instance().newPrincipal(ei.getKey(),
                    ei.getType());

            Set<PortletDefinitionBean> marketplacePortlets = new HashSet<>();
            for (IPortletDefinition portlet : portletsNotYetCategorized) {
                if (authorizationService.canPrincipalBrowse(ap, portlet)) {
                    PortletDefinitionBean pdb = preparePortletDefinitionBean(request, portlet, locale,
                            favorites.contains(portlet));
                    marketplacePortlets.add(pdb);
                }
            }

            // construct a new channel category bean for this category
            final String uncName = messageSource.getMessage(UNCATEGORIZED, new Object[] {}, locale);
            final String uncDescription = messageSource.getMessage(UNCATEGORIZED_DESC, new Object[] {}, locale);
            PortletCategory pc = new PortletCategory(uncName); // Use of this String for Id matches earlier version of API
            pc.setName(uncName);
            pc.setDescription(uncDescription);
            PortletCategoryBean unc = PortletCategoryBean.fromPortletCategory(pc, null, marketplacePortlets);

            // Add even if no portlets in category
            categories.add(unc);
        }

        rslt.put(CATEGORIES_MAP_KEY, categories);
        return rslt;
    }

    private PortletCategoryBean preparePortletCategoryBean(WebRequest req, PortletCategory category,
            Set<IPortletDefinition> portletsNotYetCategorized, IPerson user, Locale locale,
            Set<IPortletDefinition> favorites) {

        /* Prepare child categories. */
        Set<PortletCategoryBean> subcategories = new HashSet<>();
        for (PortletCategory childCategory : this.portletCategoryRegistry.getChildCategories(category)) {
            PortletCategoryBean childBean = preparePortletCategoryBean(req, childCategory,
                    portletsNotYetCategorized, user, locale, favorites);
            subcategories.add(childBean);
        }

        // add the direct child channels for this category
        Set<IPortletDefinition> portlets = portletCategoryRegistry.getChildPortlets(category);
        EntityIdentifier ei = user.getEntityIdentifier();
        IAuthorizationPrincipal ap = AuthorizationServiceFacade.instance().newPrincipal(ei.getKey(), ei.getType());

        Set<PortletDefinitionBean> marketplacePortlets = new HashSet<>();
        for (IPortletDefinition portlet : portlets) {

            if (authorizationService.canPrincipalBrowse(ap, portlet)) {
                PortletDefinitionBean pdb = preparePortletDefinitionBean(req, portlet, locale,
                        favorites.contains(portlet));
                marketplacePortlets.add(pdb);
            }

            /*
             * Remove the portlet from the uncategorized collection;
             * note -- this approach will not prevent portlets from
             * appearing in multiple categories (as appropriate).
             */
            portletsNotYetCategorized.remove(portlet);
        }

        // construct a new portlet category bean for this category
        PortletCategoryBean categoryBean = PortletCategoryBean.fromPortletCategory(category, subcategories,
                marketplacePortlets);
        categoryBean.setName(messageSource.getMessage(category.getName(), new Object[] {}, locale));

        return categoryBean;
    }

    private PortletDefinitionBean preparePortletDefinitionBean(WebRequest req, IPortletDefinition portlet,
            Locale locale, Boolean favorite) {
        MarketplacePortletDefinition mktpd = marketplaceService.getOrCreateMarketplacePortletDefinition(portlet);
        PortletDefinitionBean rslt = PortletDefinitionBean.fromMarketplacePortletDefinition(mktpd, locale,
                favorite);

        // See api docs for postProcessIconUrlParameter() below
        IPortletDefinitionParameter iconParameter = rslt.getParameters().get(ICON_URL_PARAMETER_NAME);
        if (iconParameter != null) {
            IPortletDefinitionParameter evaluated = postProcessIconUrlParameter(iconParameter, req);
            rslt.putParameter(evaluated);
        }

        return rslt;
    }

    /*
     * Implementation
     */

    private Locale getUserLocale(IPerson user) {
        // get user locale
        Locale[] locales = localeStore.getUserLocales(user);
        LocaleManager localeManager = localeManagerFactory.createLocaleManager(user, Arrays.asList(locales));
        return localeManager.getLocales().get(0);
    }

    /**
     * TODO: Clean this mess up some day; there are a few portlet-definitions that start with
     * ${request.contextPath} for the iconUrl parameter, presumably because uPortal can be deployed
     * to a context other than /uPortal. We should either...
     *
     * <p>- Discontinue SpEL in publishing parameters entirely; or - Extend it to parameters beyond
     * 'iconUrl'
     *
     * <p>And if we continue using SpEL in parameters, we should evaluate it when they're read out
     * of the database (long before now).
     *
     * <p>FWIW the /api/portlet/{fname}.json API does not process the SpEL and the
     * '${request.contextPath}' is included in the JSON output.
     */
    private IPortletDefinitionParameter postProcessIconUrlParameter(final IPortletDefinitionParameter iconUrl,
            WebRequest req) {
        if (!ICON_URL_PARAMETER_NAME.equals(iconUrl.getName())) {
            String msg = "Only iconUrl should be processed this way;  parameter was:  " + iconUrl.getName();
            throw new IllegalArgumentException(msg);
        }
        final String value = spELService.parseString(iconUrl.getValue(), req);
        return new IPortletDefinitionParameter() {
            @Override
            public String getName() {
                return ICON_URL_PARAMETER_NAME;
            }

            @Override
            public String getValue() {
                return value;
            }

            @Override
            public String getDescription() {
                return iconUrl.getDescription();
            }

            @Override
            public void setValue(String value) {
                throw new UnsupportedOperationException();
            }

            @Override
            public void setDescription(String descr) {
                throw new UnsupportedOperationException();
            }
        };
    }

    private Set<IPortletDefinition> calculateFavoritePortlets(HttpServletRequest request) {
        /*
         * It's not fantastic, but the storage strategy for favorites in the layout XML doc.  We
         * have to use it to know which portlets are favorites.
         */
        final IUserInstance ui = userInstanceManager.getUserInstance(request);
        final UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager();
        final IUserLayoutManager ulm = upm.getUserLayoutManager();
        final IUserLayout layout = ulm.getUserLayout();
        final Set<IPortletDefinition> rslt = favoritesUtils.getFavoritePortletDefinitions(layout);

        logger.debug("Found the following favoritePortlets for user='{}':  {}", request.getRemoteUser(), rslt);
        return rslt;
    }

    private Map<String, SortedSet<PortletCategoryBean>> filterRegistryFavoritesOnly(
            Map<String, SortedSet<PortletCategoryBean>> registry) {

        final Set<PortletCategoryBean> inpt = registry.get(CATEGORIES_MAP_KEY);
        final SortedSet<PortletCategoryBean> otpt = new TreeSet<>();
        inpt.forEach(categoryIn -> {
            final PortletCategoryBean categoryOut = filterCategoryFavoritesOnly(categoryIn);
            if (categoryOut != null) {
                otpt.add(categoryOut);
            }
        });

        final Map<String, SortedSet<PortletCategoryBean>> rslt = new TreeMap<>();
        rslt.put(CATEGORIES_MAP_KEY, otpt);
        return rslt;
    }

    /**
     * Returns the filtered category, or <code>null</code> if there is no content remaining in the
     * category.
     */
    private PortletCategoryBean filterCategoryFavoritesOnly(PortletCategoryBean category) {

        // Subcategories
        final Set<PortletCategoryBean> subcategories = new HashSet<>();
        category.getSubcategories().forEach(sub -> {
            final PortletCategoryBean filteredBean = filterCategoryFavoritesOnly(sub);
            if (filteredBean != null) {
                subcategories.add(filteredBean);
            }
        });

        // Portlets
        final Set<PortletDefinitionBean> portlets = new HashSet<>();
        category.getPortlets().forEach(child -> {
            if (child.getFavorite()) {
                logger.debug("Including portlet '{}' because it is a favorite:  {}", child.getFname());
                portlets.add(child);
            } else {
                logger.debug("Skipping portlet '{}' because it IS NOT a favorite:  {}", child.getFname());
            }
        });

        return !subcategories.isEmpty() || !portlets.isEmpty()
                ? PortletCategoryBean.create(category.getId(), category.getName(), category.getDescription(),
                        subcategories, portlets)
                : null;
    }
}