/**
* 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.rendering;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import java.io.Serializable;
import java.io.StringReader;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import javax.portlet.Event;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import org.apache.commons.lang.StringUtils;
import org.apache.pluto.container.PortletContainer;
import org.apache.pluto.container.PortletContainerException;
import org.apache.pluto.container.PortletWindow;
import org.apache.pluto.container.driver.PortletContextService;
import org.apache.pluto.container.om.portlet.ContainerRuntimeOption;
import org.apache.pluto.container.om.portlet.EventDefinition;
import org.apache.pluto.container.om.portlet.EventDefinitionReference;
import org.apache.pluto.container.om.portlet.PortletApplicationDefinition;
import org.apache.pluto.container.om.portlet.PortletDefinition;
import org.apereo.portal.EntityIdentifier;
import org.apereo.portal.IUserPreferencesManager;
import org.apereo.portal.layout.IUserLayoutManager;
import org.apereo.portal.portlet.container.EventImpl;
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.IPortletEntity;
import org.apereo.portal.portlet.om.IPortletEntityId;
import org.apereo.portal.portlet.om.IPortletWindow;
import org.apereo.portal.portlet.om.IPortletWindowId;
import org.apereo.portal.portlet.registry.IPortletDefinitionRegistry;
import org.apereo.portal.portlet.registry.IPortletEntityRegistry;
import org.apereo.portal.portlet.registry.IPortletWindowRegistry;
import org.apereo.portal.security.IAuthorizationPrincipal;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.services.AuthorizationService;
import org.apereo.portal.url.IPortalRequestUtils;
import org.apereo.portal.user.IUserInstance;
import org.apereo.portal.user.IUserInstanceManager;
import org.apereo.portal.utils.Tuple;
import org.apereo.portal.utils.web.PortalWebUtils;
import org.apereo.portal.xml.XmlUtilities;
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.Service;
/**
* uPortal's approach to event coordination is to simply queue the events and rely on the {@link
* IPortletExecutionManager} to handle event execution. What this class does is for each {@link
* #processEvents(PortletContainer, PortletWindow, HttpServletRequest, HttpServletResponse, List)}
* request from the portlet container is to add them to a Queue scoped to the portal's request.
*
* <p>It also provides {@link #getPortletEventQueue(PortletEventQueue, List, HttpServletRequest)}
* which is used to determine which events to send to which portlet windows.
*/
@Service("eventCoordinationService")
public class PortletEventCoordinatationService implements IPortletEventCoordinationService {
public static final String GLOBAL_EVENT__CONTAINER_OPTION = "org.apereo.portal.globalEvent";
private static final String PORTLET_EVENT_QUEUE =
PortletEventCoordinatationService.class.getName() + ".PORTLET_EVENT_QUEUE";
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
private IPortletWindowRegistry portletWindowRegistry;
private IPortletEntityRegistry portletEntityRegistry;
private IPortletDefinitionRegistry portletDefinitionRegistry;
private IUserInstanceManager userInstanceManager;
private Ehcache supportedEventCache;
private IPortalRequestUtils portalRequestUtils;
private XmlUtilities xmlUtilities;
private PortletContextService portletContextService;
@Autowired
public void setPortletContextService(PortletContextService portletContextService) {
this.portletContextService = portletContextService;
}
@Autowired
public void setXmlUtilities(XmlUtilities xmlUtilities) {
this.xmlUtilities = xmlUtilities;
}
@Autowired
public void setPortalRequestUtils(IPortalRequestUtils portalRequestUtils) {
this.portalRequestUtils = portalRequestUtils;
}
@Autowired
public void setSupportedEventCache(
@Qualifier("org.apereo.portal.portlet.rendering.SupportedEventCache")
Ehcache supportedEventCache) {
this.supportedEventCache = supportedEventCache;
}
@Autowired
public void setPortletDefinitionRegistry(IPortletDefinitionRegistry portletDefinitionRegistry) {
this.portletDefinitionRegistry = portletDefinitionRegistry;
}
@Autowired
public void setUserInstanceManager(IUserInstanceManager userInstanceManager) {
this.userInstanceManager = userInstanceManager;
}
@Autowired
public void setPortletWindowRegistry(IPortletWindowRegistry portletWindowRegistry) {
this.portletWindowRegistry = portletWindowRegistry;
}
@Autowired
public void setPortletEntityRegistry(IPortletEntityRegistry portletEntityRegistry) {
this.portletEntityRegistry = portletEntityRegistry;
}
/**
* Returns a request scoped PortletEventQueue used to track events to process and events to
* dispatch
*/
@Override
public PortletEventQueue getPortletEventQueue(HttpServletRequest request) {
request = this.portalRequestUtils.getOriginalPortalRequest(request);
synchronized (PortalWebUtils.getRequestAttributeMutex(request)) {
PortletEventQueue portletEventQueue =
(PortletEventQueue) request.getAttribute(PORTLET_EVENT_QUEUE);
if (portletEventQueue == null) {
portletEventQueue = new PortletEventQueue();
request.setAttribute(PORTLET_EVENT_QUEUE, portletEventQueue);
}
return portletEventQueue;
}
}
@Override
public void processEvents(
PortletContainer container,
PortletWindow plutoPortletWindow,
HttpServletRequest request,
HttpServletResponse response,
List<Event> events) {
final PortletEventQueue requestPortletEventQueue = this.getPortletEventQueue(request);
this.logger.debug("Queued {} from {}", events, plutoPortletWindow);
final IPortletWindow portletWindow =
this.portletWindowRegistry.convertPortletWindow(request, plutoPortletWindow);
final IPortletWindowId portletWindowId = portletWindow.getPortletWindowId();
//Add list transformer to convert Event to QueuedEvent
final List<QueuedEvent> queuedEvents =
Lists.transform(
events,
new Function<Event, QueuedEvent>() {
/* (non-Javadoc)
* @see com.google.common.base.Function#apply(java.lang.Object)
*/
@Override
public QueuedEvent apply(Event event) {
return new QueuedEvent(portletWindowId, event);
}
});
requestPortletEventQueue.addEvents(queuedEvents);
}
@Override
public void resolvePortletEvents(
HttpServletRequest request, PortletEventQueue portletEventQueue) {
final Queue<QueuedEvent> events = portletEventQueue.getUnresolvedEvents();
//Skip all processing if there are no new events.
if (events.isEmpty()) {
return;
}
//Get all the portlets the user is subscribed to
final IUserInstance userInstance = this.userInstanceManager.getUserInstance(request);
final IUserPreferencesManager preferencesManager = userInstance.getPreferencesManager();
final IUserLayoutManager userLayoutManager = preferencesManager.getUserLayoutManager();
//Make a local copy so we can remove data from it
final Set<String> allLayoutNodeIds =
new LinkedHashSet<String>(userLayoutManager.getAllSubscribedChannels());
final Map<String, IPortletEntity> portletEntityCache =
new LinkedHashMap<String, IPortletEntity>();
while (!events.isEmpty()) {
final QueuedEvent queuedEvent = events.poll();
if (queuedEvent == null) {
//no more queued events, done resolving
return;
}
final IPortletWindowId sourceWindowId = queuedEvent.getPortletWindowId();
final Event event = queuedEvent.getEvent();
final boolean globalEvent = isGlobalEvent(request, sourceWindowId, event);
final Set<IPortletDefinition> portletDefinitions =
new LinkedHashSet<IPortletDefinition>();
if (globalEvent) {
portletDefinitions.addAll(
this.portletDefinitionRegistry.getAllPortletDefinitions());
}
//Check each subscription to see what events it is registered to see
for (final Iterator<String> layoutNodeIdItr = allLayoutNodeIds.iterator();
layoutNodeIdItr.hasNext();
) {
final String layoutNodeId = layoutNodeIdItr.next();
IPortletEntity portletEntity = portletEntityCache.get(layoutNodeId);
if (portletEntity == null) {
portletEntity =
this.portletEntityRegistry.getOrCreatePortletEntity(
request, userInstance, layoutNodeId);
// if portlet entity registry returned null, then portlet has been deleted - remove it (see UP-3378)
if (portletEntity == null) {
layoutNodeIdItr.remove();
continue;
}
final IPortletDefinitionId portletDefinitionId =
portletEntity.getPortletDefinitionId();
final PortletDefinition portletDescriptor =
this.portletDefinitionRegistry.getParentPortletDescriptor(
portletDefinitionId);
if (portletDescriptor == null) {
//Missconfigured portlet, remove it from the list so we don't check again and ignore it
layoutNodeIdItr.remove();
continue;
}
final List<? extends EventDefinitionReference> supportedProcessingEvents =
portletDescriptor.getSupportedProcessingEvents();
//Skip portlets that don't handle any events and remove them from the set so they are not checked again
if (supportedProcessingEvents == null
|| supportedProcessingEvents.size() == 0) {
layoutNodeIdItr.remove();
continue;
}
portletEntityCache.put(layoutNodeId, portletEntity);
}
final IPortletDefinition portletDefinition = portletEntity.getPortletDefinition();
final IPortletDefinitionId portletDefinitionId =
portletDefinition.getPortletDefinitionId();
if (this.supportsEvent(event, portletDefinitionId)) {
this.logger.debug("{} supports event {}", portletDefinition, event);
//If this is the default portlet entity remove the definition from the all defs set to avoid duplicate processing
final IPortletEntity defaultPortletEntity =
this.portletEntityRegistry.getOrCreateDefaultPortletEntity(
request, portletDefinitionId);
if (defaultPortletEntity.equals(portletEntity)) {
portletDefinitions.remove(portletDefinition);
}
// Is this portlet permitted to receive events? (Or is it disablePortletEvents=true?)
IPortletDefinitionParameter disablePortletEvents =
portletDefinition.getParameter(
PortletExecutionManager.DISABLE_PORTLET_EVENTS_PARAMETER);
if (disablePortletEvents != null
&& Boolean.parseBoolean(disablePortletEvents.getValue())) {
logger.info(
"Ignoring portlet events for portlet '{}' because they have been disabled.",
portletDefinition.getFName());
continue;
}
final IPortletEntityId portletEntityId = portletEntity.getPortletEntityId();
final Set<IPortletWindow> portletWindows =
this.portletWindowRegistry.getAllPortletWindowsForEntity(
request, portletEntityId);
for (final IPortletWindow portletWindow : portletWindows) {
this.logger.debug("{} resolved target {}", event, portletWindow);
final IPortletWindowId portletWindowId = portletWindow.getPortletWindowId();
final Event unmarshalledEvent = this.unmarshall(portletWindow, event);
portletEventQueue.offerEvent(
portletWindowId,
new QueuedEvent(sourceWindowId, unmarshalledEvent));
}
} else {
portletDefinitions.remove(portletDefinition);
}
}
if (!portletDefinitions.isEmpty()) {
final IPerson user = userInstance.getPerson();
final EntityIdentifier ei = user.getEntityIdentifier();
final IAuthorizationPrincipal ap =
AuthorizationService.instance().newPrincipal(ei.getKey(), ei.getType());
//If the event is global there might still be portlet definitions that need targeting
for (final IPortletDefinition portletDefinition : portletDefinitions) {
// Is this portlet permitted to receive events? (Or is it disablePortletEvents=true?)
IPortletDefinitionParameter disablePortletEvents =
portletDefinition.getParameter(
PortletExecutionManager.DISABLE_PORTLET_EVENTS_PARAMETER);
if (disablePortletEvents != null
&& Boolean.parseBoolean(disablePortletEvents.getValue())) {
logger.info(
"Ignoring portlet events for portlet '{}' because they have been disabled.",
portletDefinition.getFName());
continue;
}
final IPortletDefinitionId portletDefinitionId =
portletDefinition.getPortletDefinitionId();
//Check if the user can render the portlet definition before doing event tests
if (ap.canRender(portletDefinitionId.getStringId())) {
if (this.supportsEvent(event, portletDefinitionId)) {
this.logger.debug("{} supports event {}", portletDefinition, event);
final IPortletEntity portletEntity =
this.portletEntityRegistry.getOrCreateDefaultPortletEntity(
request, portletDefinitionId);
final IPortletEntityId portletEntityId =
portletEntity.getPortletEntityId();
final Set<IPortletWindow> portletWindows =
this.portletWindowRegistry.getAllPortletWindowsForEntity(
request, portletEntityId);
for (final IPortletWindow portletWindow : portletWindows) {
this.logger.debug("{} resolved target {}", event, portletWindow);
final IPortletWindowId portletWindowId =
portletWindow.getPortletWindowId();
final Event unmarshalledEvent =
this.unmarshall(portletWindow, event);
portletEventQueue.offerEvent(
portletWindowId,
new QueuedEvent(sourceWindowId, unmarshalledEvent));
}
}
}
}
}
}
}
protected boolean isGlobalEvent(
HttpServletRequest request, IPortletWindowId sourceWindowId, Event event) {
final IPortletWindow portletWindow =
this.portletWindowRegistry.getPortletWindow(request, sourceWindowId);
final IPortletEntity portletEntity = portletWindow.getPortletEntity();
final IPortletDefinition portletDefinition = portletEntity.getPortletDefinition();
final IPortletDefinitionId portletDefinitionId = portletDefinition.getPortletDefinitionId();
final PortletApplicationDefinition parentPortletApplicationDescriptor =
this.portletDefinitionRegistry.getParentPortletApplicationDescriptor(
portletDefinitionId);
final ContainerRuntimeOption globalEvents =
parentPortletApplicationDescriptor.getContainerRuntimeOption(
GLOBAL_EVENT__CONTAINER_OPTION);
if (globalEvents != null) {
final QName qName = event.getQName();
final String qNameStr = qName.toString();
for (final String globalEvent : globalEvents.getValues()) {
if (qNameStr.equals(globalEvent)) {
return true;
}
}
}
return false;
}
protected Event unmarshall(IPortletWindow portletWindow, Event event) {
//TODO make two types of Event impls, one for marshalled data and one for unmarshalled data
String value = (String) event.getValue();
final XMLInputFactory xmlInputFactory = this.xmlUtilities.getXmlInputFactory();
final XMLStreamReader xml;
try {
xml = xmlInputFactory.createXMLStreamReader(new StringReader(value));
} catch (XMLStreamException e) {
throw new IllegalStateException(
"Failed to create XMLStreamReader for portlet event: " + event, e);
}
// now test if object is jaxb
final EventDefinition eventDefinitionDD =
getEventDefintion(portletWindow, event.getQName());
final PortletDefinition portletDefinition =
portletWindow.getPlutoPortletWindow().getPortletDefinition();
final PortletApplicationDefinition application = portletDefinition.getApplication();
final String portletApplicationName = application.getName();
final ClassLoader loader;
try {
loader = portletContextService.getClassLoader(portletApplicationName);
} catch (PortletContainerException e) {
throw new IllegalStateException(
"Failed to get ClassLoader for portlet application: " + portletApplicationName,
e);
}
final String eventType = eventDefinitionDD.getValueType();
final Class<? extends Serializable> clazz;
try {
clazz = loader.loadClass(eventType).asSubclass(Serializable.class);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(
"Declared event type '"
+ eventType
+ "' cannot be found in portlet application: "
+ portletApplicationName,
e);
}
//TODO cache JAXBContext in registered portlet application
final JAXBElement<? extends Serializable> result;
try {
final JAXBContext jc = JAXBContext.newInstance(clazz);
final Unmarshaller unmarshaller = jc.createUnmarshaller();
result = unmarshaller.unmarshal(xml, clazz);
} catch (JAXBException e) {
throw new IllegalArgumentException(
"Cannot create JAXBContext for event type '"
+ eventType
+ "' from portlet application: "
+ portletApplicationName,
e);
}
return new EventImpl(event.getQName(), result.getValue());
}
//TODO cache this resolution
protected EventDefinition getEventDefintion(IPortletWindow portletWindow, QName name) {
PortletApplicationDefinition appDD =
portletWindow.getPlutoPortletWindow().getPortletDefinition().getApplication();
for (EventDefinition def : appDD.getEventDefinitions()) {
if (def.getQName() != null) {
if (def.getQName().equals(name)) return def;
} else {
QName tmp = new QName(appDD.getDefaultNamespace(), def.getName());
if (tmp.equals(name)) return def;
}
}
throw new IllegalStateException();
}
protected Set<QName> getAllAliases(
QName eventName, PortletApplicationDefinition portletApplicationDefinition) {
final List<? extends EventDefinition> eventDefinitions =
portletApplicationDefinition.getEventDefinitions();
if (eventDefinitions == null || eventDefinitions.isEmpty()) {
return Collections.emptySet();
}
final String defaultNamespace = portletApplicationDefinition.getDefaultNamespace();
for (final EventDefinition eventDefinition : eventDefinitions) {
final QName defQName = eventDefinition.getQualifiedName(defaultNamespace);
if (defQName != null && defQName.equals(eventName)) {
final List<QName> aliases = eventDefinition.getAliases();
if (aliases == null || aliases.isEmpty()) {
return Collections.emptySet();
}
return new LinkedHashSet<QName>(aliases);
}
}
return Collections.emptySet();
}
protected boolean supportsEvent(Event event, IPortletDefinitionId portletDefinitionId) {
final QName eventName = event.getQName();
//The cache key to use
final Tuple<IPortletDefinitionId, QName> key =
new Tuple<IPortletDefinitionId, QName>(portletDefinitionId, eventName);
//Check in the cache if the portlet definition supports this event
final Element element = this.supportedEventCache.get(key);
if (element != null) {
final Boolean supported = (Boolean) element.getObjectValue();
if (supported != null) {
return supported;
}
}
final PortletApplicationDefinition portletApplicationDescriptor =
this.portletDefinitionRegistry.getParentPortletApplicationDescriptor(
portletDefinitionId);
if (portletApplicationDescriptor == null) {
return false;
}
final Set<QName> aliases = this.getAllAliases(eventName, portletApplicationDescriptor);
final String defaultNamespace = portletApplicationDescriptor.getDefaultNamespace();
//No support found so far, do more complex namespace matching
final PortletDefinition portletDescriptor =
this.portletDefinitionRegistry.getParentPortletDescriptor(portletDefinitionId);
if (portletDescriptor == null) {
return false;
}
final List<? extends EventDefinitionReference> supportedProcessingEvents =
portletDescriptor.getSupportedProcessingEvents();
for (final EventDefinitionReference eventDefinitionReference : supportedProcessingEvents) {
final QName qualifiedName = eventDefinitionReference.getQualifiedName(defaultNamespace);
if (qualifiedName == null) {
continue;
}
//See if the supported qname and event qname match explicitly
//Look for alias names
if (qualifiedName.equals(eventName) || aliases.contains(qualifiedName)) {
this.supportedEventCache.put(new Element(key, Boolean.TRUE));
return true;
}
//Look for namespaced events
if (StringUtils.isEmpty(qualifiedName.getNamespaceURI())) {
final QName namespacedName =
new QName(defaultNamespace, qualifiedName.getLocalPart());
if (eventName.equals(namespacedName)) {
this.supportedEventCache.put(new Element(key, Boolean.TRUE));
return true;
}
}
}
this.supportedEventCache.put(new Element(key, Boolean.FALSE));
return false;
}
}