/**
* 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 java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apereo.portal.EntityIdentifier;
import org.apereo.portal.portlet.om.IPortletDefinition;
import org.apereo.portal.portlet.om.IPortletDefinitionId;
import org.apereo.portal.portlet.om.IPortletDefinitionParameter;
import org.apereo.portal.portlet.om.IPortletDescriptorKey;
import org.apereo.portal.portlet.om.IPortletPreference;
import org.apereo.portal.portlet.om.IPortletType;
import org.apereo.portal.portlet.om.PortletCategory;
import org.apereo.portal.portlet.om.PortletLifecycleState;
import org.apereo.portal.portlet.registry.IPortletCategoryRegistry;
import org.apereo.portal.security.AuthorizationPrincipalHelper;
import org.apereo.portal.security.IAuthorizationPrincipal;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.utils.web.PortalWebUtils;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Marketplace portlet definitions add Marketplace-specific features upon an underlying
* IPortletDefinition.
*
* @since 4.1
*/
public class MarketplacePortletDefinition implements IPortletDefinition {
public static final String MARKETPLACE_FNAME = "portletmarketplace";
public static final int QUANTITY_RELATED_PORTLETS_TO_SHOW = 5;
private static final String INITIAL_RELEASE_DATE_PREFERENCE_NAME = "Initial_Release_Date";
private static final String RELEASE_DATE_PREFERENCE_NAME = "Release_Date";
private static final String RELEASE_NOTE_PREFERENCE_NAME = "Release_Notes";
private static final String RELEASE_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static final String MARKETPLACE_SHORT_LINK_PREF = "short_link";
private static final String MARKETPLACE_KEYWORDS_PREF = "keywords";
private static final DateTimeFormatter releaseDateFormatter =
DateTimeFormat.forPattern(RELEASE_DATE_FORMAT);
protected final Logger logger = LoggerFactory.getLogger(getClass());
private IMarketplaceService marketplaceService;
private IPortletCategoryRegistry portletCategoryRegistry;
private IPortletDefinition portletDefinition;
private List<ScreenShot> screenShots;
private PortletReleaseNotes releaseNotes;
private Set<PortletCategory> categories;
// that MarketplacePortletDefinition contains more MarketplacePortletDefinition is okay only so long as
// related portlets are lazy-initialized on access and are not always accessed.
private Set<MarketplacePortletDefinition> relatedPortlets;
private Set<PortletCategory> parentCategories;
private String shortURL = null;
private List<String> keywords = null;
/**
* @param portletDefinition the portlet definition to make this MarketplacePD
* @param registry the registry you want to use for categories and related apps Benefit:
* screenshot property is set if any are available. This includes URL and caption
*/
public MarketplacePortletDefinition(
final IPortletDefinition portletDefinition,
final IMarketplaceService service,
final IPortletCategoryRegistry registry) {
this.portletCategoryRegistry = registry;
this.marketplaceService = service;
this.portletDefinition = portletDefinition;
this.initDefinitions();
}
private void initDefinitions() {
this.initScreenShots();
this.initPortletReleaseNotes();
this.initCategories();
this.initShortURL();
this.initKeywords();
// deliberately does *not* init related portlets, to avoid infinite recursion of initing related portlets.
// TODO: use a caching registry so that get some cache hits when referencing MarketplacePortletDefinitions
}
private void initKeywords() {
List<IPortletPreference> portletPreferences =
this.portletDefinition.getPortletPreferences();
for (IPortletPreference pref : portletPreferences) {
if (MARKETPLACE_KEYWORDS_PREF.equalsIgnoreCase(pref.getName())) {
this.keywords = new ArrayList<String>(Arrays.asList(pref.getValues()));
break;
}
}
}
private void initShortURL() {
List<IPortletPreference> portletPreferences =
this.portletDefinition.getPortletPreferences();
for (IPortletPreference pref : portletPreferences) {
if (MARKETPLACE_SHORT_LINK_PREF.equalsIgnoreCase(pref.getName())
&& pref.getValues().length == 1) {
setShortURL(pref.getValues()[0]);
break;
}
}
}
private void initScreenShots() {
List<IPortletPreference> portletPreferences =
this.portletDefinition.getPortletPreferences();
List<IPortletPreference> urls =
new ArrayList<IPortletPreference>(portletPreferences.size());
List<IPortletPreference> captions =
new ArrayList<IPortletPreference>(portletPreferences.size());
List<ScreenShot> screenShots = new ArrayList<ScreenShot>();
//Creates a list of captions and list of urls
for (int i = 0; i < portletPreferences.size(); i++) {
//Most screenshots possible is i
urls.add(null);
captions.add(null);
for (int j = 0; j < portletPreferences.size(); j++) {
if (portletPreferences
.get(j)
.getName()
.equalsIgnoreCase("screen_shot" + Integer.toString(i + 1))) {
urls.set(i, portletPreferences.get(j));
}
if (portletPreferences
.get(j)
.getName()
.equalsIgnoreCase("screen_shot" + Integer.toString(i + 1) + "_caption")) {
captions.set(i, portletPreferences.get(j));
}
}
}
for (int i = 0; i < urls.size(); i++) {
if (urls.get(i) != null) {
if (captions.size() > i && captions.get(i) != null) {
screenShots.add(
new ScreenShot(
urls.get(i).getValues()[0],
Arrays.asList(captions.get(i).getValues())));
} else {
screenShots.add(new ScreenShot(urls.get(i).getValues()[0]));
}
}
}
this.setScreenShots(screenShots);
}
private void initPortletReleaseNotes() {
PortletReleaseNotes temp = new PortletReleaseNotes();
for (IPortletPreference portletPreference :
this.portletDefinition.getPortletPreferences()) {
if (MarketplacePortletDefinition.RELEASE_DATE_PREFERENCE_NAME.equalsIgnoreCase(
portletPreference.getName())) {
try {
DateTime dt =
releaseDateFormatter.parseDateTime(portletPreference.getValues()[0]);
temp.setReleaseDate(dt);
} catch (Exception e) {
logger.warn(
"Issue with parsing "
+ RELEASE_DATE_PREFERENCE_NAME
+ ". Should be in format "
+ RELEASE_DATE_FORMAT,
e);
}
continue;
}
if (MarketplacePortletDefinition.RELEASE_NOTE_PREFERENCE_NAME.equalsIgnoreCase(
portletPreference.getName())) {
temp.setReleaseNotes(Arrays.asList(portletPreference.getValues()));
continue;
}
if (MarketplacePortletDefinition.INITIAL_RELEASE_DATE_PREFERENCE_NAME.equalsIgnoreCase(
portletPreference.getName())) {
try {
DateTime dt =
releaseDateFormatter.parseDateTime(portletPreference.getValues()[0]);
temp.setInitialReleaseDate(dt);
} catch (Exception e) {
logger.warn(
"Issue with parsing "
+ INITIAL_RELEASE_DATE_PREFERENCE_NAME
+ ". Should be in format "
+ RELEASE_DATE_FORMAT,
e);
}
continue;
}
}
this.setPortletReleaseNotes(temp);
}
/**
* private method that sets the parentCategories field and the categories field This will ensure
* that the public methods {@link #getParentCategories() getParentCategories()} and {@link
* #getCategories() getCategories()} will not return null. Empty sets are allowed
*/
private void initCategories() {
Set<PortletCategory> allCategories = new HashSet<PortletCategory>();
this.setParentCategories(this.portletCategoryRegistry.getParentCategories(this));
for (PortletCategory childCategory : this.parentCategories) {
allCategories.add(childCategory);
allCategories.addAll(
this.portletCategoryRegistry.getAllParentCategories(childCategory));
}
this.setCategories(allCategories);
}
/**
* Initialize related portlets. This must be called lazily so that MarketplacePortletDefinitions
* instantiated as related portlets off of a MarketplacePortletDefinition do not always
* instantiate their related MarketplacePortletDefinitions, ad infinitem.
*/
private void initRelatedPortlets() {
final Set<MarketplacePortletDefinition> allRelatedPortlets = new HashSet<>();
for (PortletCategory parentCategory :
this.portletCategoryRegistry.getParentCategories(this)) {
final Set<IPortletDefinition> portletsInCategory =
this.portletCategoryRegistry.getAllChildPortlets(parentCategory);
for (IPortletDefinition portletDefinition : portletsInCategory) {
allRelatedPortlets.add(
new MarketplacePortletDefinition(
portletDefinition,
this.marketplaceService,
this.portletCategoryRegistry));
}
}
allRelatedPortlets.remove(this);
this.relatedPortlets = allRelatedPortlets;
}
private void setScreenShots(List<ScreenShot> screenShots) {
this.screenShots = screenShots;
}
/** @return the screenShotList. Will not be null. */
public List<ScreenShot> getScreenShots() {
if (screenShots == null) {
this.initScreenShots();
}
return screenShots;
}
private void setPortletReleaseNotes(PortletReleaseNotes portletReleaseNotes) {
this.releaseNotes = portletReleaseNotes;
}
/** @return the releaseNotes. Will not be null. */
public PortletReleaseNotes getPortletReleaseNotes() {
if (releaseNotes == null) {
this.initPortletReleaseNotes();
}
return releaseNotes;
}
private void setCategories(Set<PortletCategory> categories) {
this.categories = categories;
}
/**
* @return a set of categories that includes all the categories that this definition is in and
* all parent categories of the portlet's category. Will not return null. Might return empty
* set.
*/
public Set<PortletCategory> getCategories() {
if (this.categories == null) {
this.initCategories();
}
return this.categories;
}
private void setParentCategories(Set<PortletCategory> parentCategories) {
this.parentCategories = parentCategories;
}
/**
* @return a set of categories that this portlet directly belongs too. Will not traverse up the
* category tree and return grandparent categories and farther. Will not return null. Might
* return empty set.
*/
public Set<PortletCategory> getParentCategories() {
if (this.parentCategories == null) {
this.initCategories();
}
return this.parentCategories;
}
/**
* @return a set of portlets that include all portlets in this portlets immediate categories and
* children categories Will not return null. Will not include self in list. Might return an
* empty set.
*/
public Set<MarketplacePortletDefinition> getRelatedPortlets() {
// lazy init is essential to avoid infinite recursion in graphing related portlets.
if (this.relatedPortlets == null) {
this.initRelatedPortlets();
}
return this.relatedPortlets;
}
/**
* Obtain up to QUANTITY_RELATED_PORTLETS_TO_SHOW random related portlets BROWSEable by the
* given user.
*
* @return a non-null potentially empty Set of related portlets BROWSEable by the user
*/
public Set<MarketplacePortletDefinition> getRandomSamplingRelatedPortlets(final IPerson user) {
Validate.notNull(user, "Cannot filter to BROWSEable by a null user");
final IAuthorizationPrincipal principal =
AuthorizationPrincipalHelper.principalFromUser(user);
// lazy init is essential to avoid infinite recursion in graphing related portlets.
if (this.relatedPortlets == null) {
this.initRelatedPortlets();
}
if (this.relatedPortlets.isEmpty()) {
return this.relatedPortlets;
}
List<MarketplacePortletDefinition> tempList =
new ArrayList<MarketplacePortletDefinition>(this.relatedPortlets);
Collections.shuffle(tempList);
final int count = Math.min(QUANTITY_RELATED_PORTLETS_TO_SHOW, tempList.size());
final Set<MarketplacePortletDefinition> rslt = new HashSet<MarketplacePortletDefinition>();
for (final MarketplacePortletDefinition relatedPortlet : tempList) {
if (marketplaceService.mayBrowsePortlet(principal, relatedPortlet)) {
rslt.add(relatedPortlet);
}
if (rslt.size() >= count) break; // escape the loop if we've hit our target quantity
// of related portlets
}
return rslt;
}
/**
* Convenience method for getting a bookmarkable URL for rendering the defined portlet,
*
* <p>Normally this is the portal rendering of the portlet, addressed by fname, and in that case
* this method will *ONLY* work when invoked in the context of a Spring-managed
* HttpServletRequest available via RequestContextHolder.
*
* <p>When there is an alternativeMaximizedLink, this method returns that instead.
*
* <p>WARNING: This method does not consider whether the requesting user has permission to
* render the target portlet, so this might be getting a URL that the user can't actually use.
*
* @return URL for rendering the portlet
* @throws IllegalStateException when context does not allow computing the URL
*/
public String getRenderUrl() {
final String alternativeMaximizedUrl = getAlternativeMaximizedLink();
if (null != alternativeMaximizedUrl) {
return alternativeMaximizedUrl;
}
final String contextPath = PortalWebUtils.currentRequestContextPath();
// TODO: stop abstraction violation of relying on knowledge of uPortal URL implementation details
return contextPath + "/p/" + getFName() + "/render.uP";
}
@Override
public String getDataId() {
return this.portletDefinition.getDataId();
}
@Override
public String getDataTitle() {
return this.portletDefinition.getDataTitle();
}
@Override
public String getDataDescription() {
return this.portletDefinition.getDataDescription();
}
@Override
public IPortletDefinitionId getPortletDefinitionId() {
return this.portletDefinition.getPortletDefinitionId();
}
@Override
public List<IPortletPreference> getPortletPreferences() {
return this.portletDefinition.getPortletPreferences();
}
@Override
public boolean setPortletPreferences(List<IPortletPreference> portletPreferences) {
return this.portletDefinition.setPortletPreferences(portletPreferences);
}
@Override
public PortletLifecycleState getLifecycleState() {
return this.portletDefinition.getLifecycleState();
}
@Override
public String getFName() {
return this.portletDefinition.getFName();
}
@Override
public String getName() {
return this.portletDefinition.getName();
}
@Override
public String getDescription() {
return this.portletDefinition.getDescription();
}
@Override
public IPortletDescriptorKey getPortletDescriptorKey() {
return this.portletDefinition.getPortletDescriptorKey();
}
@Override
public String getTitle() {
return this.portletDefinition.getTitle();
}
@Override
public int getTimeout() {
return this.portletDefinition.getTimeout();
}
@Override
public Integer getActionTimeout() {
return this.portletDefinition.getActionTimeout();
}
@Override
public Integer getEventTimeout() {
return this.portletDefinition.getEventTimeout();
}
@Override
public Integer getRenderTimeout() {
return this.portletDefinition.getRenderTimeout();
}
@Override
public Integer getResourceTimeout() {
return this.portletDefinition.getResourceTimeout();
}
@Override
public IPortletType getType() {
return this.portletDefinition.getType();
}
@Override
public int getPublisherId() {
return this.portletDefinition.getPublisherId();
}
@Override
public int getApproverId() {
return this.portletDefinition.getApproverId();
}
@Override
public Date getPublishDate() {
return this.portletDefinition.getPublishDate();
}
@Override
public Date getApprovalDate() {
return this.portletDefinition.getApprovalDate();
}
@Override
public int getExpirerId() {
return this.portletDefinition.getExpirerId();
}
@Override
public Date getExpirationDate() {
return this.portletDefinition.getExpirationDate();
}
@Override
public Set<IPortletDefinitionParameter> getParameters() {
return this.portletDefinition.getParameters();
}
@Override
public IPortletDefinitionParameter getParameter(String key) {
return this.portletDefinition.getParameter(key);
}
@Override
public Map<String, IPortletDefinitionParameter> getParametersAsUnmodifiableMap() {
return this.portletDefinition.getParametersAsUnmodifiableMap();
}
@Override
public String getName(String locale) {
return this.portletDefinition.getName();
}
@Override
public String getDescription(String locale) {
return this.portletDefinition.getDescription();
}
@Override
public String getTitle(String locale) {
return this.portletDefinition.getTitle(locale);
}
@Override
public String getAlternativeMaximizedLink() {
return this.portletDefinition.getAlternativeMaximizedLink();
}
@Override
public void setFName(String fname) {
this.portletDefinition.setFName(fname);
}
@Override
public void setName(String name) {
this.portletDefinition.setName(name);
}
@Override
public void setDescription(String descr) {
this.portletDefinition.setDescription(descr);
}
@Override
public void setTitle(String title) {
this.portletDefinition.setTitle(title);
}
@Override
public void setTimeout(int timeout) {
this.portletDefinition.setTimeout(timeout);
}
@Override
public void setActionTimeout(Integer actionTimeout) {
this.portletDefinition.setActionTimeout(actionTimeout);
}
@Override
public void setEventTimeout(Integer eventTimeout) {
this.portletDefinition.setEventTimeout(eventTimeout);
}
@Override
public void setRenderTimeout(Integer renderTimeout) {
this.portletDefinition.setRenderTimeout(renderTimeout);
}
@Override
public void setResourceTimeout(Integer resourceTimeout) {
this.portletDefinition.setResourceTimeout(resourceTimeout);
}
@Override
public void setType(IPortletType channelType) {
this.portletDefinition.setType(channelType);
}
@Override
public void setPublisherId(int publisherId) {
this.portletDefinition.setPublisherId(publisherId);
}
@Override
public void setApproverId(int approvalId) {
this.portletDefinition.setApproverId(approvalId);
}
@Override
public void setPublishDate(Date publishDate) {
this.portletDefinition.setPublishDate(publishDate);
}
@Override
public void setApprovalDate(Date approvalDate) {
this.portletDefinition.setApprovalDate(approvalDate);
}
@Override
public void setExpirerId(int expirerId) {
this.portletDefinition.setExpirerId(expirerId);
}
@Override
public void setExpirationDate(Date expirationDate) {
this.portletDefinition.setExpirationDate(expirationDate);
}
@Override
public void setParameters(Set<IPortletDefinitionParameter> parameters) {
this.portletDefinition.setParameters(parameters);
}
@Override
public void addLocalizedTitle(String locale, String chanTitle) {
this.portletDefinition.addLocalizedTitle(locale, chanTitle);
}
@Override
public void addLocalizedName(String locale, String chanName) {
this.portletDefinition.addLocalizedName(locale, chanName);
}
@Override
public void addLocalizedDescription(String locale, String chanDesc) {
this.portletDefinition.addLocalizedDescription(locale, chanDesc);
}
@Override
public EntityIdentifier getEntityIdentifier() {
return this.portletDefinition.getEntityIdentifier();
}
@Override
public void addParameter(IPortletDefinitionParameter parameter) {
this.portletDefinition.addParameter(parameter);
}
@Override
public void addParameter(String name, String value) {
this.portletDefinition.addParameter(name, value);
}
@Override
public void removeParameter(IPortletDefinitionParameter parameter) {
this.portletDefinition.removeParameter(parameter);
}
@Override
public void removeParameter(String name) {
this.portletDefinition.removeParameter(name);
}
@Override
public Double getRating() {
return this.portletDefinition.getRating();
}
@Override
public void setRating(Double rating) {
this.portletDefinition.setRating(rating);
}
@Override
public Long getUsersRated() {
return this.portletDefinition.getUsersRated();
}
@Override
public void setUsersRated(Long usersRated) {
this.portletDefinition.setUsersRated(usersRated);
}
public String getShortURL() {
return shortURL;
}
public void setShortURL(String shortURL) {
this.shortURL = shortURL;
}
@Override
public String getTarget() {
return this.portletDefinition.getTarget();
}
@Override
public String toString() {
return new ToStringBuilder(this).append("fname", getFName()).toString();
}
/*
* Marketplace portlet definitions are definitively identified by the fname of their underlying
* portlet publication, so only the fname contributes to the hashcode.
* @since 4.2
*/
@Override
public int hashCode() {
return getFName().hashCode();
}
/*
* Equal where the other object is a MarketplacePortletDefinition with the same fname.
* This is important so that Set operations work properly.
* @since 4.2
*/
@Override
public boolean equals(Object other) {
if (null == other) {
return false;
}
if (this == other) {
return true;
}
if (getClass() != other.getClass()) {
return false;
}
final MarketplacePortletDefinition otherDefinition = (MarketplacePortletDefinition) other;
if (getFName() == otherDefinition.getFName()) {
return true;
}
; // both null fname case
return getFName().equals(otherDefinition.getFName());
}
public List<String> getKeywords() {
return keywords;
}
}