/**
* 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.rendering;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.ser.BeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.portlet.WindowState;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apereo.portal.character.stream.CharacterEventReader;
import org.apereo.portal.character.stream.FilteringCharacterEventReader;
import org.apereo.portal.character.stream.events.CharacterDataEventImpl;
import org.apereo.portal.character.stream.events.CharacterEvent;
import org.apereo.portal.events.PortalEvent;
import org.apereo.portal.events.PortletRenderExecutionEvent;
import org.apereo.portal.events.RequestScopedEventsTracker;
import org.apereo.portal.events.aggr.tabs.AggregatedTabLookupDao;
import org.apereo.portal.events.aggr.tabs.AggregatedTabMapping;
import org.apereo.portal.portlet.om.IPortletWindowId;
import org.apereo.portal.spring.beans.factory.ObjectMapperFactoryBean;
import org.apereo.portal.url.IPortalRequestInfo;
import org.apereo.portal.url.IUrlSyntaxProvider;
import org.apereo.portal.utils.cache.CacheKey;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
public class AnalyticsIncorporationComponent extends CharacterPipelineComponentWrapper
implements InitializingBean {
private ObjectMapper mapper;
private ObjectWriter portletEventWriter;
private AggregatedTabLookupDao aggregatedTabLookupDao;
private IUrlSyntaxProvider urlSyntaxProvider;
private RequestScopedEventsTracker requestScopedEventsTracker;
@JsonFilter(PortletRenderExecutionEventFilterMixIn.FILTER_NAME)
private interface PortletRenderExecutionEventFilterMixIn {
static final String FILTER_NAME = "PortletRenderExecutionEventFilter";
}
//Ignored until https://github.com/FasterXML/jackson-databind/issues/245 is fixed
//Delete the mapper related code in afterPropertiesSet once the issue is fixed
// @Autowired
// public void setMapper(ObjectMapper mapper) {
//
// //Clone the mapper so that our mixins don't break other code
// this.mapper = mapper.copy();
// initMapper();
// }
@Override
public void afterPropertiesSet() throws Exception {
final ObjectMapperFactoryBean omfb = new ObjectMapperFactoryBean();
omfb.afterPropertiesSet();
this.mapper = omfb.getObject();
initMapper();
}
/**
* Configure the ObjectMapper to filter out all fields on the events except those that are
* actually needed for the analytics reporting
*/
private void initMapper() {
final BeanPropertyFilter filterOutAllExcept =
SimpleBeanPropertyFilter.filterOutAllExcept("fname", "executionTimeNano");
this.mapper.addMixInAnnotations(
PortalEvent.class, PortletRenderExecutionEventFilterMixIn.class);
final SimpleFilterProvider filterProvider = new SimpleFilterProvider();
filterProvider.addFilter(
PortletRenderExecutionEventFilterMixIn.FILTER_NAME, filterOutAllExcept);
this.portletEventWriter = this.mapper.writer(filterProvider);
}
@Autowired
public void setAggregatedTabLookupDao(AggregatedTabLookupDao aggregatedTabLookupDao) {
this.aggregatedTabLookupDao = aggregatedTabLookupDao;
}
@Autowired
public void setUrlSyntaxProvider(IUrlSyntaxProvider urlSyntaxProvider) {
this.urlSyntaxProvider = urlSyntaxProvider;
}
@Autowired
public void setRequestScopedEventsTracker(
RequestScopedEventsTracker requestScopedEventsTracker) {
this.requestScopedEventsTracker = requestScopedEventsTracker;
}
@Override
public CacheKey getCacheKey(HttpServletRequest request, HttpServletResponse response) {
return this.wrappedComponent.getCacheKey(request, response);
}
@Override
public PipelineEventReader<CharacterEventReader, CharacterEvent> getEventReader(
HttpServletRequest request, HttpServletResponse response) {
final long startTime = System.nanoTime();
final PipelineEventReader<CharacterEventReader, CharacterEvent> pipelineEventReader =
this.wrappedComponent.getEventReader(request, response);
final CharacterEventReader eventReader = pipelineEventReader.getEventReader();
final AnalyticsIncorporatingEventReader portletIncorporatingEventReader =
new AnalyticsIncorporatingEventReader(eventReader, request, startTime);
final Map<String, String> outputProperties = pipelineEventReader.getOutputProperties();
return new PipelineEventReaderImpl<CharacterEventReader, CharacterEvent>(
portletIncorporatingEventReader, outputProperties);
}
protected String serializePortletRenderExecutionEvents(final Set<PortalEvent> portalEvents) {
//Filter to include just portlet render events
final Map<String, PortletRenderExecutionEvent> renderEvents =
new HashMap<String, PortletRenderExecutionEvent>();
for (final PortalEvent portalEvent : portalEvents) {
if (portalEvent instanceof PortletRenderExecutionEvent) {
final PortletRenderExecutionEvent portletRenderEvent =
(PortletRenderExecutionEvent) portalEvent;
//Don't write out info for minimized portlets
if (!WindowState.MINIMIZED.equals(portletRenderEvent.getWindowState())) {
final IPortletWindowId portletWindowId =
portletRenderEvent.getPortletWindowId();
final String eventKey =
portletWindowId != null
? portletWindowId.getStringId()
: portletRenderEvent.getFname();
renderEvents.put(eventKey, portletRenderEvent);
}
}
}
try {
return portletEventWriter.writeValueAsString(renderEvents);
} catch (JsonParseException e) {
logger.warn(
"Failed to convert this request's render events to JSON, no portlet level analytics will be included",
e);
} catch (JsonMappingException e) {
logger.warn(
"Failed to convert this request's render events to JSON, no portlet level analytics will be included",
e);
} catch (IOException e) {
logger.warn(
"Failed to convert this request's render events to JSON, no portlet level analytics will be included",
e);
}
return "{}";
}
protected String serializePageData(HttpServletRequest request, long startTime) {
final Map<String, Object> pageData = new HashMap<String, Object>();
pageData.put("executionTimeNano", System.nanoTime() - startTime);
final IPortalRequestInfo portalRequestInfo =
urlSyntaxProvider.getPortalRequestInfo(request);
pageData.put("urlState", portalRequestInfo.getUrlState());
final String targetedLayoutNodeId = portalRequestInfo.getTargetedLayoutNodeId();
if (targetedLayoutNodeId != null) {
final AggregatedTabMapping mappedTabForLayoutId =
aggregatedTabLookupDao.getMappedTabForLayoutId(targetedLayoutNodeId);
pageData.put("tab", mappedTabForLayoutId);
}
try {
return mapper.writeValueAsString(pageData);
} catch (JsonParseException e) {
logger.warn(
"Failed to convert this request's page data to JSON, no page level analytics will be included",
e);
} catch (JsonMappingException e) {
logger.warn(
"Failed to convert this request's page data to JSON, no page level analytics will be included",
e);
} catch (IOException e) {
logger.warn(
"Failed to convert this request's page data to JSON, no page level analytics will be included",
e);
}
return "{}";
}
private class AnalyticsIncorporatingEventReader extends FilteringCharacterEventReader {
private final HttpServletRequest request;
private final long startTime;
public AnalyticsIncorporatingEventReader(
CharacterEventReader delegate, HttpServletRequest request, final long startTime) {
super(delegate);
this.request = request;
this.startTime = startTime;
}
@Override
protected CharacterEvent filterEvent(CharacterEvent event, boolean peek) {
switch (event.getEventType()) {
case PORTLET_ANALYTICS_DATA:
{
//Get the set of events for the request
final Set<PortalEvent> portalEvents =
requestScopedEventsTracker.getRequestEvents(request);
final String data = serializePortletRenderExecutionEvents(portalEvents);
return CharacterDataEventImpl.create(data);
}
case PAGE_ANALYTICS_DATA:
{
final String data = serializePageData(request, startTime);
return CharacterDataEventImpl.create(data);
}
default:
{
return event;
}
}
}
}
}