/** * 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.events.aggr; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.apereo.portal.events.PortalEvent; import org.apereo.portal.events.aggr.groups.AggregatedGroupMapping; import org.apereo.portal.events.aggr.session.EventSession; import org.apereo.portal.jpa.BaseAggrEventsJpaDao.AggrEventsTransactional; import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; /** * Base {@link PortalEvent} aggregator, useful for aggregations that extend from {@link * BaseAggregationImpl} * * @param <E> The {@link PortalEvent} type handled by this aggregator * @param <T> The {@link BaseAggregationImpl} subclass operated on by this aggregator * @param <K> The {@link BaseAggregationKey} type used by this aggregator */ public abstract class BaseIntervalAwarePortalEventAggregator< E extends PortalEvent, T extends BaseAggregationImpl<K, ?>, K extends BaseAggregationKey> extends BasePortalEventAggregator<E> implements IntervalAwarePortalEventAggregator<E> { private final String aggregationsCacheKey = this.getClass().getName() + ".AGGREGATIONS_FOR_INTERVAL"; private AggregationIntervalHelper aggregationIntervalHelper; @Autowired public void setAggregationIntervalHelper(AggregationIntervalHelper aggregationIntervalHelper) { this.aggregationIntervalHelper = aggregationIntervalHelper; } /** @return The private aggregation DAO to use */ protected abstract BaseAggregationPrivateDao<T, K> getAggregationDao(); /** * Called for each {@link BaseAggregationImpl} that needs to be updated * * @param e The {@link PortalEvent} to get the data from * @param intervalInfo The info about the interval the aggregation is for * @param aggregation The aggregation to update */ protected abstract void updateAggregation( E e, EventAggregationContext eventAggregationContext, AggregationIntervalInfo intervalInfo, T aggregation); /** * Create a unique key that describes the aggregation. * * @param intervalInfo The info about the interval the aggregation is for * @param aggregatedGroup The group the aggregation is for * @param e The event the aggregation is for */ protected abstract K createAggregationKey( E e, EventAggregationContext eventAggregationContext, AggregationIntervalInfo intervalInfo, AggregatedGroupMapping aggregatedGroup); @AggrEventsTransactional @Override public final void aggregateEvent( E e, EventSession eventSession, EventAggregationContext eventAggregationContext, Map<AggregationInterval, AggregationIntervalInfo> currentIntervals) { final BaseAggregationPrivateDao<T, K> aggregationDao = this.getAggregationDao(); for (Map.Entry<AggregationInterval, AggregationIntervalInfo> intervalInfoEntry : currentIntervals.entrySet()) { final AggregationIntervalInfo intervalInfo = intervalInfoEntry.getValue(); //Map used to cache aggregations locally after loading Map<K, T> aggregationsCache = eventAggregationContext.getAttribute(this.aggregationsCacheKey); if (aggregationsCache == null) { aggregationsCache = new HashMap<K, T>(); eventAggregationContext.setAttribute(this.aggregationsCacheKey, aggregationsCache); } //Groups this event is for final Set<AggregatedGroupMapping> groupMappings = eventSession.getGroupMappings(); //For each group get/create then update the aggregation for (final AggregatedGroupMapping groupMapping : groupMappings) { final K key = this.createAggregationKey( e, eventAggregationContext, intervalInfo, groupMapping); //Load the aggregation, try from the cache first T aggregation = aggregationsCache.get(key); if (aggregation == null) { //Then try loading from the db aggregation = aggregationDao.getAggregation(key); if (aggregation == null) { //Finally create the aggregation aggregation = aggregationDao.createAggregation(key); } //Store the loaded/created aggregation in the local cache aggregationsCache.put(key, aggregation); } //Update the aggregation with the event updateAggregation(e, eventAggregationContext, intervalInfo, aggregation); } } } @AggrEventsTransactional @Override public final void handleIntervalBoundary( AggregationInterval interval, EventAggregationContext eventAggregationContext, Map<AggregationInterval, AggregationIntervalInfo> intervals) { final AggregationIntervalInfo intervalInfo = intervals.get(interval); final BaseAggregationPrivateDao<T, K> aggregationDao = this.getAggregationDao(); //Complete all of the aggregations that have been touched by this session, can be null if no events of //the handled type have been seen so far in this session Map<K, T> aggregationsForInterval = eventAggregationContext.getAttribute(this.aggregationsCacheKey); if (aggregationsForInterval == null) { //No aggregations have been seen in this interval, nothing to do return; } //Tracks the aggregations that need to be updated, estimate size based on intervals/aggregations ratio final Collection<T> updatedAggregations = new ArrayList<T>(aggregationsForInterval.size() / intervals.size()); //Mark each aggregation that matches the interval complete and remove it from the map of tracked aggregations final Collection<T> aggregations = aggregationsForInterval.values(); for (final Iterator<T> aggregationItr = aggregations.iterator(); aggregationItr.hasNext(); ) { final T aggregation = aggregationItr.next(); if (aggregation.getInterval() == interval) { final int duration = intervalInfo.getTotalDuration(); aggregation.intervalComplete(duration); aggregationItr.remove(); updatedAggregations.add(aggregation); } } //Instruct the DAO to remove the aggregation from cache after updating, once closed it will never be visited again aggregationDao.updateAggregations(updatedAggregations, true); } @AggrEventsTransactional @Override public int cleanUnclosedAggregations( DateTime start, DateTime end, AggregationInterval interval) { final BaseAggregationPrivateDao<T, K> aggregationDao = this.getAggregationDao(); final Collection<T> unclosedAggregations = aggregationDao.getUnclosedAggregations(start, end, interval); for (final T aggregation : unclosedAggregations) { final DateTime eventDate = aggregation.getDateTime(); final AggregationIntervalInfo unclosedIntervalInfo = this.aggregationIntervalHelper.getIntervalInfo(interval, eventDate); aggregation.intervalComplete(unclosedIntervalInfo.getTotalDuration()); } aggregationDao.updateAggregations(unclosedAggregations, true); return unclosedAggregations.size(); } }