/** * 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.List; import org.apereo.portal.concurrency.locking.IClusterLockService; import org.apereo.portal.events.aggr.dao.DateDimensionDao; import org.apereo.portal.events.aggr.dao.IEventAggregationManagementDao; import org.apereo.portal.events.aggr.dao.TimeDimensionDao; import org.apereo.portal.events.handlers.db.IPortalEventDao; import org.apereo.portal.jpa.BaseAggrEventsJpaDao; import org.joda.time.DateMidnight; import org.joda.time.DateTime; import org.joda.time.LocalTime; import org.joda.time.Period; import org.joda.time.ReadablePeriod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service public class PortalEventDimensionPopulatorImpl extends BaseAggrEventsJpaDao implements DisposableBean, PortalEventDimensionPopulator { protected final Logger logger = LoggerFactory.getLogger(getClass()); private TimeDimensionDao timeDimensionDao; private DateDimensionDao dateDimensionDao; private AggregationIntervalHelper intervalHelper; private IPortalEventDao portalEventDao; private IEventAggregationManagementDao eventAggregationManagementDao; private IClusterLockService clusterLockService; private ReadablePeriod dimensionBuffer = Period.days(30); private volatile boolean checkedDimensions = false; private volatile boolean shutdown = false; @Autowired public void setClusterLockService(IClusterLockService clusterLockService) { this.clusterLockService = clusterLockService; } @Autowired public void setTimeDimensionDao(TimeDimensionDao timeDimensionDao) { this.timeDimensionDao = timeDimensionDao; } @Autowired public void setDateDimensionDao(DateDimensionDao dateDimensionDao) { this.dateDimensionDao = dateDimensionDao; } @Autowired public void setIntervalHelper(AggregationIntervalHelper intervalHelper) { this.intervalHelper = intervalHelper; } @Autowired public void setPortalEventDao(IPortalEventDao portalEventDao) { this.portalEventDao = portalEventDao; } @Autowired public void setEventAggregationManagementDao( IEventAggregationManagementDao eventAggregationManagementDao) { this.eventAggregationManagementDao = eventAggregationManagementDao; } @Value( "${org.apereo.portal.events.aggr.PortalEventDimensionPopulatorImpl.dimensionBuffer:P30D}") public void setDimensionBuffer(ReadablePeriod dimensionBuffer) { if (new Period(dimensionBuffer).toStandardDays().getDays() < 1) { throw new IllegalArgumentException( "dimensionBuffer must be at least 1 day. Is: " + new Period(dimensionBuffer).toStandardDays().getDays()); } this.dimensionBuffer = dimensionBuffer; } @Override public void destroy() throws Exception { this.shutdown = true; } @Override public boolean isCheckedDimensions() { return checkedDimensions; } @Override @AggrEventsTransactional public void doPopulateDimensions() { if (shutdown) { logger.warn("populateDimensions called after shutdown, ignoring call"); } if (!this.clusterLockService.isLockOwner(DIMENSION_LOCK_NAME)) { throw new IllegalStateException( "The cluster lock " + DIMENSION_LOCK_NAME + " must be owned by the current thread and server"); } doPopulateTimeDimensions(); doPopulateDateDimensions(); doUpdateDateDimensions(); //Immediately flush all date/time dimension changes to the database this.getEntityManager().flush(); this.checkedDimensions = true; } private void checkShutdown() { if (shutdown) { //Mark ourselves as interupted and throw an exception Thread.currentThread().interrupt(); throw new RuntimeException( "uPortal is shutting down, throwing an exeption to stop processing"); } } /** Populate the time dimensions */ final void doPopulateTimeDimensions() { final List<TimeDimension> timeDimensions = this.timeDimensionDao.getTimeDimensions(); if (timeDimensions.isEmpty()) { logger.info("No TimeDimensions exist, creating them"); } else if (timeDimensions.size() != (24 * 60)) { this.logger.info( "There are only " + timeDimensions.size() + " time dimensions in the database, there should be " + (24 * 60) + " creating missing dimensions"); } else { this.logger.debug("Found expected " + timeDimensions.size() + " time dimensions"); return; } LocalTime nextTime = new LocalTime(0, 0); final LocalTime lastTime = new LocalTime(23, 59); for (final TimeDimension timeDimension : timeDimensions) { LocalTime dimensionTime = timeDimension.getTime(); if (nextTime.isBefore(dimensionTime)) { do { checkShutdown(); this.timeDimensionDao.createTimeDimension(nextTime); nextTime = nextTime.plusMinutes(1); } while (nextTime.isBefore(dimensionTime)); } else if (nextTime.isAfter(dimensionTime)) { do { checkShutdown(); this.timeDimensionDao.createTimeDimension(dimensionTime); dimensionTime = dimensionTime.plusMinutes(1); } while (nextTime.isAfter(dimensionTime)); } nextTime = dimensionTime.plusMinutes(1); } //Add any missing times from the tail while (nextTime.isBefore(lastTime) || nextTime.equals(lastTime)) { checkShutdown(); this.timeDimensionDao.createTimeDimension(nextTime); if (nextTime.equals(lastTime)) { break; } nextTime = nextTime.plusMinutes(1); } } final void doPopulateDateDimensions() { final DateTime now = getNow(); final AggregationIntervalInfo startIntervalInfo; final DateTime oldestPortalEventTimestamp = this.portalEventDao.getOldestPortalEventTimestamp(); if (oldestPortalEventTimestamp == null || now.isBefore(oldestPortalEventTimestamp)) { startIntervalInfo = this.intervalHelper.getIntervalInfo( AggregationInterval.YEAR, now.minus(this.dimensionBuffer)); } else { startIntervalInfo = this.intervalHelper.getIntervalInfo( AggregationInterval.YEAR, oldestPortalEventTimestamp.minus(this.dimensionBuffer)); } final AggregationIntervalInfo endIntervalInfo; final DateTime newestPortalEventTimestamp = this.portalEventDao.getNewestPortalEventTimestamp(); if (newestPortalEventTimestamp == null || now.isAfter(newestPortalEventTimestamp)) { endIntervalInfo = this.intervalHelper.getIntervalInfo( AggregationInterval.YEAR, now.plus(this.dimensionBuffer)); } else { endIntervalInfo = this.intervalHelper.getIntervalInfo( AggregationInterval.YEAR, newestPortalEventTimestamp.plus(this.dimensionBuffer)); } final DateMidnight start = startIntervalInfo.getStart().toDateMidnight(); final DateMidnight end = endIntervalInfo.getEnd().toDateMidnight(); doPopulateDateDimensions(start, end); } final void doPopulateDateDimensions(final DateMidnight start, final DateMidnight end) { logger.info("Populating DateDimensions between {} and {}", start, end); final List<QuarterDetail> quartersDetails = this.eventAggregationManagementDao.getQuartersDetails(); final List<AcademicTermDetail> academicTermDetails = this.eventAggregationManagementDao.getAcademicTermDetails(); final List<DateDimension> dateDimensions = this.dateDimensionDao.getDateDimensionsBetween(start, end); DateMidnight nextDate = start; for (final DateDimension dateDimension : dateDimensions) { DateMidnight dimensionDate = dateDimension.getDate(); if (nextDate.isBefore(dimensionDate)) { do { checkShutdown(); createDateDimension(quartersDetails, academicTermDetails, nextDate); nextDate = nextDate.plusDays(1); } while (nextDate.isBefore(dimensionDate)); } else if (nextDate.isAfter(dimensionDate)) { do { checkShutdown(); createDateDimension(quartersDetails, academicTermDetails, dimensionDate); dimensionDate = dimensionDate.plusDays(1); } while (nextDate.isAfter(dimensionDate)); } nextDate = dimensionDate.plusDays(1); } //Add any missing dates from the tail while (nextDate.isBefore(end)) { checkShutdown(); createDateDimension(quartersDetails, academicTermDetails, nextDate); nextDate = nextDate.plusDays(1); } } /** Populate the term/quarter data for dimensions that are missing the data */ final void doUpdateDateDimensions() { final List<DateDimension> dateDimensions = this.dateDimensionDao.getDateDimensionsWithoutTerm(); final List<AcademicTermDetail> academicTermDetails = this.eventAggregationManagementDao.getAcademicTermDetails(); for (final DateDimension dateDimension : dateDimensions) { final DateMidnight date = dateDimension.getDate(); final AcademicTermDetail termDetail = EventDateTimeUtils.findDateRangeSorted(date, academicTermDetails); if (termDetail != null) { dateDimension.setTerm(termDetail.getTermName()); this.dateDimensionDao.updateDateDimension(dateDimension); } } } /** Exists to make this class testable */ DateTime getNow() { return DateTime.now(); } /** Creates a date dimension, handling the quarter and term lookup logic */ protected void createDateDimension( List<QuarterDetail> quartersDetails, List<AcademicTermDetail> academicTermDetails, DateMidnight date) { final QuarterDetail quarterDetail = EventDateTimeUtils.findDateRangeSorted(date, quartersDetails); final AcademicTermDetail termDetail = EventDateTimeUtils.findDateRangeSorted(date, academicTermDetails); this.dateDimensionDao.createDateDimension( date, quarterDetail.getQuarterId(), termDetail != null ? termDetail.getTermName() : null); } }