/** * 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 com.google.common.base.Function; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Join; import javax.persistence.criteria.JoinType; import javax.persistence.criteria.ParameterExpression; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import org.apereo.portal.events.aggr.dao.jpa.DateDimensionImpl; import org.apereo.portal.events.aggr.dao.jpa.DateDimensionImpl_; import org.apereo.portal.events.aggr.dao.jpa.TimeDimensionImpl; import org.apereo.portal.events.aggr.dao.jpa.TimeDimensionImpl_; import org.apereo.portal.events.aggr.groups.AggregatedGroupMapping; import org.apereo.portal.events.aggr.groups.AggregatedGroupMappingImpl; import org.apereo.portal.jpa.BaseAggrEventsJpaDao; import org.apereo.portal.jpa.OpenEntityManager; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.joda.time.LocalTime; import org.springframework.beans.factory.annotation.Autowired; /** * Base for JPA DAOs that handle {@link BaseAggregationImpl} subclasses. Provides impls of the * standard methods defined by {@link BaseAggregationPrivateDao}. Note that subclasses MUST call * {@link #afterPropertiesSet()} if they override it or this class WILL NOT WORK * * @param <T> The entity type being aggregated * @param <K> The entity primary key */ public abstract class JpaBaseAggregationDao< T extends BaseAggregationImpl<K, ?>, K extends BaseAggregationKey> extends BaseAggrEventsJpaDao implements BaseAggregationPrivateDao<T, K> { private final Class<T> aggregationEntityType; private HibernateCacheEvictor hibernateCacheEvictor; protected CriteriaQuery<T> findAggregationByDateTimeIntervalQuery; protected CriteriaQuery<T> findAggregationByDateTimeIntervalGroupQuery; protected CriteriaQuery<T> findAggregationsByDateRangeQuery; protected CriteriaQuery<T> findUnclosedAggregationsByDateRangeQuery; protected CriteriaQuery<AggregationInterval> findAggregationIntervalsQuery; protected CriteriaQuery<AggregatedGroupMappingImpl> findAggregatedGroupsQuery; protected ParameterExpression<TimeDimension> timeDimensionParameter; protected ParameterExpression<DateDimension> dateDimensionParameter; protected ParameterExpression<AggregationInterval> intervalParameter; protected ParameterExpression<AggregatedGroupMapping> aggregatedGroupParameter; protected ParameterExpression<Set> aggregatedGroupsParameter; protected ParameterExpression<LocalDate> startDate; protected ParameterExpression<LocalDate> endPlusOneDate; protected ParameterExpression<LocalDate> endDate; protected ParameterExpression<LocalTime> startTime; protected ParameterExpression<LocalTime> endTime; public JpaBaseAggregationDao(Class<T> aggregationEntityType) { this.aggregationEntityType = aggregationEntityType; } @Autowired public void setHibernateCacheEvictor(HibernateCacheEvictor hibernateCacheEvictor) { this.hibernateCacheEvictor = hibernateCacheEvictor; } /** * Add any fetches needed for the following queries: findAggregationByDateTimeIntervalQuery * findUnclosedAggregationsByDateRangeQuery */ protected abstract void addFetches(Root<T> root); /** Add the additional predicate needed to find the unclosed aggregates */ protected abstract void addUnclosedPredicate( CriteriaBuilder cb, Root<T> root, List<Predicate> keyPredicates); /** Add the additional predicate needed if using an extension of {@link BaseAggregationKey} */ protected void addAggregationSpecificKeyPredicate( CriteriaBuilder cb, Root<T> root, List<Predicate> keyPredicates) {} /** * Bind the non-standard key parameters from the extension of {@link BaseAggregationKey} for * standard queries */ protected void bindAggregationSpecificKeyParameters(TypedQuery<T> query, Set<K> keys) {} /** * Bind the non-standard key parameters from the extension of {@link BaseAggregationKey} for * natual id queries */ protected void bindAggregationSpecificKeyParameters(NaturalIdQuery<T> query, K key) {} /** For subclasses to use to create additional {@link ParameterExpression}s */ protected void createParameterExpressions() {} /** For subclasses to use to create additional {@link CriteriaQuery}s */ protected void createCriteriaQueries() {} /** Create a new aggregation instance */ protected abstract T createAggregationInstance(K key); /** Get the aggregation key for this instance */ protected abstract K getAggregationKey(T instance); @Override public final void afterPropertiesSet() throws Exception { this.timeDimensionParameter = this.createParameterExpression(TimeDimension.class, "timeDimension"); this.dateDimensionParameter = this.createParameterExpression(DateDimension.class, "dateDimension"); this.intervalParameter = this.createParameterExpression(AggregationInterval.class, "interval"); this.aggregatedGroupParameter = this.createParameterExpression(AggregatedGroupMapping.class, "aggregatedGroup"); this.aggregatedGroupsParameter = this.createParameterExpression(Set.class, "aggregatedGroups"); this.startDate = this.createParameterExpression(LocalDate.class, "startDate"); this.endPlusOneDate = this.createParameterExpression(LocalDate.class, "endPlusOneDate"); this.endDate = this.createParameterExpression(LocalDate.class, "endDate"); this.startTime = this.createParameterExpression(LocalTime.class, "startTime"); this.endTime = this.createParameterExpression(LocalTime.class, "endTime"); this.createParameterExpressions(); this.findAggregationByDateTimeIntervalQuery = this.createCriteriaQuery( new Function<CriteriaBuilder, CriteriaQuery<T>>() { @Override public CriteriaQuery<T> apply(CriteriaBuilder cb) { final CriteriaQuery<T> criteriaQuery = cb.createQuery(aggregationEntityType); final Root<T> ba = criteriaQuery.from(aggregationEntityType); addFetches(ba); criteriaQuery.select(ba); criteriaQuery.where( cb.equal( ba.get(BaseAggregationImpl_.dateDimension), dateDimensionParameter), cb.equal( ba.get(BaseAggregationImpl_.timeDimension), timeDimensionParameter), cb.equal( ba.get(BaseAggregationImpl_.interval), intervalParameter)); return criteriaQuery; } }); this.findAggregationByDateTimeIntervalGroupQuery = this.createCriteriaQuery( new Function<CriteriaBuilder, CriteriaQuery<T>>() { @Override public CriteriaQuery<T> apply(CriteriaBuilder cb) { final CriteriaQuery<T> criteriaQuery = cb.createQuery(aggregationEntityType); final Root<T> ba = criteriaQuery.from(aggregationEntityType); final List<Predicate> keyPredicates = new ArrayList<Predicate>(); keyPredicates.add( cb.equal( ba.get(BaseAggregationImpl_.dateDimension), dateDimensionParameter)); keyPredicates.add( cb.equal( ba.get(BaseAggregationImpl_.timeDimension), timeDimensionParameter)); keyPredicates.add( cb.equal( ba.get(BaseAggregationImpl_.interval), intervalParameter)); keyPredicates.add( cb.equal( ba.get(BaseAggregationImpl_.aggregatedGroup), aggregatedGroupParameter)); addAggregationSpecificKeyPredicate(cb, ba, keyPredicates); criteriaQuery.select(ba); criteriaQuery.where( keyPredicates.toArray(new Predicate[keyPredicates.size()])); return criteriaQuery; } }); this.findAggregationsByDateRangeQuery = this.createCriteriaQuery( new Function<CriteriaBuilder, CriteriaQuery<T>>() { @Override public CriteriaQuery<T> apply(CriteriaBuilder cb) { final CriteriaQuery<T> criteriaQuery = cb.createQuery(aggregationEntityType); final Root<T> ba = criteriaQuery.from(aggregationEntityType); final Join<T, DateDimensionImpl> dd = ba.join(BaseAggregationImpl_.dateDimension, JoinType.LEFT); final Join<T, TimeDimensionImpl> td = ba.join(BaseAggregationImpl_.timeDimension, JoinType.LEFT); final List<Predicate> keyPredicates = new ArrayList<Predicate>(); keyPredicates.add( cb.and( //Restrict results by outer date range cb.greaterThanOrEqualTo( dd.get(DateDimensionImpl_.date), startDate), cb.lessThan( dd.get(DateDimensionImpl_.date), endPlusOneDate))); keyPredicates.add( cb.or( //Restrict start of range by time as well cb.greaterThan( dd.get(DateDimensionImpl_.date), startDate), cb.greaterThanOrEqualTo( td.get(TimeDimensionImpl_.time), startTime))); keyPredicates.add( cb.or( //Restrict end of range by time as well cb.lessThan( dd.get(DateDimensionImpl_.date), endDate), cb.lessThan( td.get(TimeDimensionImpl_.time), endTime))); keyPredicates.add( cb.equal( ba.get(BaseAggregationImpl_.interval), intervalParameter)); keyPredicates.add( ba.get(BaseAggregationImpl_.aggregatedGroup) .in(aggregatedGroupsParameter)); addAggregationSpecificKeyPredicate(cb, ba, keyPredicates); criteriaQuery.select(ba); criteriaQuery.where( keyPredicates.toArray(new Predicate[keyPredicates.size()])); criteriaQuery.orderBy( cb.desc(dd.get(DateDimensionImpl_.date)), cb.desc(td.get(TimeDimensionImpl_.time))); return criteriaQuery; } }); /* * Similar to the previous query but only returns aggregates that also match the unclosed predicate generated * by the subclass. This is used for finding aggregates that missed having intervalComplete called due to * interval boundary placement. */ this.findUnclosedAggregationsByDateRangeQuery = this.createCriteriaQuery( new Function<CriteriaBuilder, CriteriaQuery<T>>() { @Override public CriteriaQuery<T> apply(CriteriaBuilder cb) { final CriteriaQuery<T> criteriaQuery = cb.createQuery(aggregationEntityType); final Root<T> ba = criteriaQuery.from(aggregationEntityType); final Join<T, DateDimensionImpl> dd = ba.join(BaseAggregationImpl_.dateDimension, JoinType.LEFT); final Join<T, TimeDimensionImpl> td = ba.join(BaseAggregationImpl_.timeDimension, JoinType.LEFT); addFetches(ba); final List<Predicate> keyPredicates = new ArrayList<Predicate>(); keyPredicates.add( cb.and( //Restrict results by outer date range cb.greaterThanOrEqualTo( dd.get(DateDimensionImpl_.date), startDate), cb.lessThan( dd.get(DateDimensionImpl_.date), endDate))); keyPredicates.add( cb.or( //Restrict start of range by time as well cb.greaterThan( dd.get(DateDimensionImpl_.date), startDate), cb.greaterThanOrEqualTo( td.get(TimeDimensionImpl_.time), startTime))); keyPredicates.add( cb.or( //Restrict end of range by time as well cb.lessThan( dd.get(DateDimensionImpl_.date), endPlusOneDate), cb.lessThan( td.get(TimeDimensionImpl_.time), endTime))); keyPredicates.add( cb.equal( ba.get(BaseAggregationImpl_.interval), intervalParameter)); //No aggregation specific key bits here, we only have start/end/interval parameters to work with addUnclosedPredicate(cb, ba, keyPredicates); criteriaQuery.select(ba); criteriaQuery.where( keyPredicates.toArray(new Predicate[keyPredicates.size()])); return criteriaQuery; } }); this.findAggregationIntervalsQuery = this.createCriteriaQuery( new Function<CriteriaBuilder, CriteriaQuery<AggregationInterval>>() { @Override public CriteriaQuery<AggregationInterval> apply(CriteriaBuilder cb) { final CriteriaQuery<AggregationInterval> criteriaQuery = cb.createQuery(AggregationInterval.class); final Root<T> ba = criteriaQuery.from(aggregationEntityType); criteriaQuery.distinct(true); criteriaQuery.select(ba.get(BaseAggregationImpl_.interval)); return criteriaQuery; } }); this.findAggregatedGroupsQuery = this.createCriteriaQuery( new Function<CriteriaBuilder, CriteriaQuery<AggregatedGroupMappingImpl>>() { @Override public CriteriaQuery<AggregatedGroupMappingImpl> apply( CriteriaBuilder cb) { final CriteriaQuery<AggregatedGroupMappingImpl> criteriaQuery = cb.createQuery(AggregatedGroupMappingImpl.class); final Root<T> ba = criteriaQuery.from(aggregationEntityType); criteriaQuery.distinct(true); criteriaQuery.select(ba.get(BaseAggregationImpl_.aggregatedGroup)); return criteriaQuery; } }); this.createCriteriaQueries(); } @Override public final List<T> getAggregations( DateTime start, DateTime end, K key, AggregatedGroupMapping... aggregatedGroupMappings) { Set<K> keys = new HashSet<K>(); keys.add(key); return getAggregations(start, end, keys, aggregatedGroupMappings); } @Override public final List<T> getAggregations( DateTime start, DateTime end, Set<K> keys, AggregatedGroupMapping... aggregatedGroupMappings) { if (!start.isBefore(end)) { throw new IllegalArgumentException("Start must be before End: " + start + " - " + end); } final LocalDate startDate = start.toLocalDate(); final LocalDate endDate = end.toLocalDate(); final TypedQuery<T> query = this.createQuery(findAggregationsByDateRangeQuery); query.setParameter(this.startDate, startDate); query.setParameter(this.startTime, start.toLocalTime()); query.setParameter(this.endDate, endDate); query.setParameter(this.endTime, end.toLocalTime()); query.setParameter(this.endPlusOneDate, endDate.plusDays(1)); // Get the first key to use for the interval K key = keys.iterator().next(); query.setParameter(this.intervalParameter, key.getInterval()); this.bindAggregationSpecificKeyParameters(query, keys); final Set<AggregatedGroupMapping> groups = collectAllGroupsFromParams(keys, aggregatedGroupMappings); query.setParameter(this.aggregatedGroupsParameter, groups); return query.getResultList(); } // Create set of all aggregatedGroups in both keys and those passed in as a parameter // and set in query. protected final Set<AggregatedGroupMapping> collectAllGroupsFromParams( Set<K> keys, AggregatedGroupMapping[] aggregatedGroupMappings) { final Builder<AggregatedGroupMapping> groupsBuilder = ImmutableSet.<AggregatedGroupMapping>builder(); // Add all groups from the keyset for (K aggregationKey : keys) { groupsBuilder.add(aggregationKey.getAggregatedGroup()); } // Add groups from parameters groupsBuilder.add(aggregatedGroupMappings); return groupsBuilder.build(); } @Override public final Map<K, T> getAggregationsForInterval( DateDimension dateDimension, TimeDimension timeDimension, AggregationInterval interval) { final TypedQuery<T> query = this.createQuery(this.findAggregationByDateTimeIntervalQuery); query.setParameter(this.dateDimensionParameter, dateDimension); query.setParameter(this.timeDimensionParameter, timeDimension); query.setParameter(this.intervalParameter, interval); final List<T> results = query.getResultList(); final Map<K, T> resultMap = new HashMap<K, T>(); for (final T result : results) { final K key = this.getAggregationKey(result); resultMap.put(key, result); } return resultMap; } @Override public final Set<AggregationInterval> getAggregationIntervals() { final TypedQuery<AggregationInterval> query = this.createQuery(this.findAggregationIntervalsQuery); return Sets.immutableEnumSet(query.getResultList()); } @Override public final Set<AggregatedGroupMapping> getAggregatedGroupMappings() { final TypedQuery<AggregatedGroupMappingImpl> query = this.createQuery(this.findAggregatedGroupsQuery); return ImmutableSet.<AggregatedGroupMapping>copyOf(query.getResultList()); } @Override @OpenEntityManager(unitName = PERSISTENCE_UNIT_NAME) public final T getAggregation(K key) { final NaturalIdQuery<T> query = this.createNaturalIdQuery(this.aggregationEntityType); query.using(BaseAggregationImpl_.dateDimension, (DateDimensionImpl) key.getDateDimension()); query.using(BaseAggregationImpl_.timeDimension, (TimeDimensionImpl) key.getTimeDimension()); query.using(BaseAggregationImpl_.interval, key.getInterval()); query.using( BaseAggregationImpl_.aggregatedGroup, (AggregatedGroupMappingImpl) key.getAggregatedGroup()); this.bindAggregationSpecificKeyParameters(query, key); return query.load(); } @Override public Collection<T> getUnclosedAggregations( DateTime start, DateTime end, AggregationInterval interval) { if (!start.isBefore(end)) { throw new IllegalArgumentException("Start must be before End: " + start + " - " + end); } final LocalDate startDate = start.toLocalDate(); final LocalDate endDate = end.toLocalDate(); final TypedQuery<T> query = this.createQuery(findUnclosedAggregationsByDateRangeQuery); query.setParameter(this.startDate, startDate); query.setParameter(this.startTime, start.toLocalTime()); query.setParameter(this.endDate, endDate); query.setParameter(this.endTime, end.toLocalTime()); query.setParameter(this.endPlusOneDate, endDate.plusDays(1)); query.setParameter(this.intervalParameter, interval); //Need set to handle duplicate results from join return new LinkedHashSet<T>(query.getResultList()); } @AggrEventsTransactional @Override public final T createAggregation(K key) { final T aggregation = createAggregationInstance(key); this.getEntityManager().persist(aggregation); return aggregation; } @AggrEventsTransactional @Override public final void updateAggregation(T aggregation) { this.getEntityManager().persist(aggregation); } @AggrEventsTransactional @Override public final void updateAggregations(Iterable<T> aggregations, boolean removeFromCache) { final EntityManager entityManager = this.getEntityManager(); for (final T aggregation : aggregations) { entityManager.persist(aggregation); if (removeFromCache) { this.hibernateCacheEvictor.evictEntity(aggregation.getClass(), aggregation.getId()); } } } }