/**
* 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.handlers.db;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import java.io.IOException;
import java.util.List;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.ParameterExpression;
import javax.persistence.criteria.Root;
import org.apereo.portal.concurrency.FunctionWithoutResult;
import org.apereo.portal.events.PortalEvent;
import org.apereo.portal.jpa.BaseRawEventsJpaDao;
import org.hibernate.FlushMode;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.stereotype.Repository;
/**
* Stores portal events using JPA/Hibenate no internal batch segmentation is done to the passed list
* of {@link PortalEvent}s. If a {@link PortalEvent} is not mapped as a persistent entity a message
* is logged at the WARN level and the event is ignored.
*
*/
@Repository
public class JpaPortalEventStore extends BaseRawEventsJpaDao implements IPortalEventDao {
private ObjectMapper mapper;
private String deleteQuery;
private String selectQuery;
private String selectUnaggregatedQuery;
private int flushPeriod = 1000;
private CriteriaQuery<DateTime> findNewestPersistentPortalEventTimestampQuery;
private CriteriaQuery<DateTime> findOldestPersistentPortalEventTimestampQuery;
private ParameterExpression<DateTime> startTimeParameter;
private ParameterExpression<DateTime> endTimeParameter;
@Autowired
public void setMapper(ObjectMapper mapper) {
this.mapper = mapper;
}
/**
* Frequency that updated events should be flushed during a call to {@link
* #aggregatePortalEvents(DateTime, DateTime, int, FunctionWithoutResult)}, defaults to 1000.
*/
@Value(
"${org.apereo.portal.events.handlers.db.JpaPortalEventStore.aggregationFlushPeriod:1000}")
public void setAggregationFlushPeriod(int flushPeriod) {
this.flushPeriod = flushPeriod;
}
@Override
public void afterPropertiesSet() throws Exception {
this.startTimeParameter = this.createParameterExpression(DateTime.class, "startTime");
this.endTimeParameter = this.createParameterExpression(DateTime.class, "endTime");
this.selectQuery =
"SELECT e "
+ "FROM "
+ PersistentPortalEvent.class.getName()
+ " e "
+ "WHERE e."
+ PersistentPortalEvent_.timestamp.getName()
+ " >= :"
+ this.startTimeParameter.getName()
+ " "
+ "AND e."
+ PersistentPortalEvent_.timestamp.getName()
+ " < :"
+ this.endTimeParameter.getName()
+ " "
+ "ORDER BY e."
+ PersistentPortalEvent_.timestamp.getName()
+ " ASC";
this.selectUnaggregatedQuery =
"SELECT e "
+ "FROM "
+ PersistentPortalEvent.class.getName()
+ " e "
+ "WHERE e."
+ PersistentPortalEvent_.timestamp.getName()
+ " >= :"
+ this.startTimeParameter.getName()
+ " "
+ "AND e."
+ PersistentPortalEvent_.timestamp.getName()
+ " < :"
+ this.endTimeParameter.getName()
+ " "
+ "AND (e."
+ PersistentPortalEvent_.aggregated.getName()
+ " is null OR e."
+ PersistentPortalEvent_.aggregated.getName()
+ " = false) "
+ "AND (e."
+ PersistentPortalEvent_.errorAggregating.getName()
+ " is null OR e."
+ PersistentPortalEvent_.errorAggregating.getName()
+ " = false) "
+ "ORDER BY e."
+ PersistentPortalEvent_.timestamp.getName()
+ " ASC";
this.deleteQuery =
"DELETE FROM "
+ PersistentPortalEvent.class.getName()
+ " e "
+ "WHERE e."
+ PersistentPortalEvent_.timestamp.getName()
+ " < :"
+ this.endTimeParameter.getName();
this.findNewestPersistentPortalEventTimestampQuery =
this.createCriteriaQuery(
new Function<CriteriaBuilder, CriteriaQuery<DateTime>>() {
@Override
public CriteriaQuery<DateTime> apply(CriteriaBuilder cb) {
final CriteriaQuery<DateTime> criteriaQuery =
cb.createQuery(DateTime.class);
final Root<PersistentPortalEvent> eventRoot =
criteriaQuery.from(PersistentPortalEvent.class);
//Get the largest event timestamp
criteriaQuery.select(
cb.greatest(
eventRoot.get(PersistentPortalEvent_.timestamp)));
return criteriaQuery;
}
});
this.findOldestPersistentPortalEventTimestampQuery =
this.createCriteriaQuery(
new Function<CriteriaBuilder, CriteriaQuery<DateTime>>() {
@Override
public CriteriaQuery<DateTime> apply(CriteriaBuilder cb) {
final CriteriaQuery<DateTime> criteriaQuery =
cb.createQuery(DateTime.class);
final Root<PersistentPortalEvent> eventRoot =
criteriaQuery.from(PersistentPortalEvent.class);
//Get the smallest event timestamp
criteriaQuery.select(
cb.least(eventRoot.get(PersistentPortalEvent_.timestamp)));
return criteriaQuery;
}
});
}
@Override
@RawEventsTransactional
public void storePortalEvent(PortalEvent portalEvent) {
final PersistentPortalEvent persistentPortalEvent = this.wrapPortalEvent(portalEvent);
this.getEntityManager().persist(persistentPortalEvent);
}
@Override
@RawEventsTransactional
public void storePortalEvents(PortalEvent... portalEvents) {
for (final PortalEvent portalEvent : portalEvents) {
try {
storePortalEvent(portalEvent);
} catch (IllegalArgumentException iae) {
this.logger.warn(
portalEvent.getClass().getName()
+ " is not mapped as a persistent entity and will not be stored. "
+ portalEvent
+ " Exception="
+ iae.getMessage());
}
}
}
@Override
@RawEventsTransactional
public void storePortalEvents(Iterable<PortalEvent> portalEvents) {
for (final PortalEvent portalEvent : portalEvents) {
try {
storePortalEvent(portalEvent);
} catch (IllegalArgumentException iae) {
this.logger.warn(
portalEvent.getClass().getName()
+ " is not mapped as a persistent entity and will not be stored. "
+ portalEvent
+ " Exception="
+ iae.getMessage());
}
}
}
@Override
public DateTime getOldestPortalEventTimestamp() {
final TypedQuery<DateTime> query =
this.createQuery(this.findOldestPersistentPortalEventTimestampQuery);
final List<DateTime> results = query.getResultList();
return DataAccessUtils.uniqueResult(results);
}
@Override
public DateTime getNewestPortalEventTimestamp() {
final TypedQuery<DateTime> query =
this.createQuery(this.findNewestPersistentPortalEventTimestampQuery);
final List<DateTime> results = query.getResultList();
return DataAccessUtils.uniqueResult(results);
}
@Override
@RawEventsTransactional
public boolean aggregatePortalEvents(
DateTime startTime,
DateTime endTime,
int maxEvents,
Function<PortalEvent, Boolean> handler) {
final Session session = this.getEntityManager().unwrap(Session.class);
session.setFlushMode(FlushMode.COMMIT);
final org.hibernate.Query query = session.createQuery(this.selectUnaggregatedQuery);
query.setParameter(this.startTimeParameter.getName(), startTime);
query.setParameter(this.endTimeParameter.getName(), endTime);
if (maxEvents > 0) {
query.setMaxResults(maxEvents);
}
int resultCount = 0;
for (final ScrollableResults results = query.scroll(ScrollMode.FORWARD_ONLY);
results.next();
) {
final PersistentPortalEvent persistentPortalEvent =
(PersistentPortalEvent) results.get(0);
final PortalEvent portalEvent;
try {
portalEvent =
this.toPortalEvent(
persistentPortalEvent.getEventData(),
persistentPortalEvent.getEventType());
} catch (RuntimeException e) {
this.logger.warn(
"Failed to convert PersistentPortalEvent to PortalEvent: "
+ persistentPortalEvent,
e);
//Mark the event as error and store the mark to prevent trying to reprocess the broken event data
persistentPortalEvent.setErrorAggregating(true);
session.persist(persistentPortalEvent);
continue;
}
try {
final Boolean eventHandled = handler.apply(portalEvent);
if (!eventHandled) {
this.logger.debug(
"Aggregation stop requested before processing event {}", portalEvent);
return false;
}
//Mark the event as aggregated and store the mark
persistentPortalEvent.setAggregated(true);
session.persist(persistentPortalEvent);
//periodic flush and clear of session to manage memory demands
if (++resultCount % this.flushPeriod == 0) {
this.logger.debug(
"Aggregated {} events, flush and clear {} EntityManager.",
resultCount,
PERSISTENCE_UNIT_NAME);
session.flush();
session.clear();
}
} catch (Exception e) {
this.logger.warn("Failed to aggregate portal event: " + persistentPortalEvent, e);
//mark the event as erred and move on. This will not be picked up by processing again
persistentPortalEvent.setErrorAggregating(true);
session.persist(persistentPortalEvent);
}
}
return true;
}
@Override
public void getPortalEvents(
DateTime startTime, DateTime endTime, FunctionWithoutResult<PortalEvent> handler) {
this.getPortalEvents(startTime, endTime, -1, handler);
}
@Override
public void getPortalEvents(
DateTime startTime,
DateTime endTime,
int maxEvents,
FunctionWithoutResult<PortalEvent> handler) {
final Session session = this.getEntityManager().unwrap(Session.class);
final org.hibernate.Query query = session.createQuery(this.selectQuery);
query.setParameter(this.startTimeParameter.getName(), startTime);
query.setParameter(this.endTimeParameter.getName(), endTime);
if (maxEvents > 0) {
query.setMaxResults(maxEvents);
}
for (final ScrollableResults results = query.scroll(ScrollMode.FORWARD_ONLY);
results.next();
) {
final PersistentPortalEvent persistentPortalEvent =
(PersistentPortalEvent) results.get(0);
final PortalEvent portalEvent =
this.toPortalEvent(
persistentPortalEvent.getEventData(),
persistentPortalEvent.getEventType());
handler.apply(portalEvent);
persistentPortalEvent.setAggregated(true);
session.evict(persistentPortalEvent);
}
}
@Override
@RawEventsTransactional
public int deletePortalEventsBefore(DateTime time) {
final Query query = this.getEntityManager().createQuery(this.deleteQuery);
query.setParameter(this.endTimeParameter.getName(), time);
return query.executeUpdate();
}
protected PersistentPortalEvent wrapPortalEvent(PortalEvent event) {
final String portalEventData = this.toString(event);
return new PersistentPortalEvent(event, portalEventData);
}
protected <E extends PortalEvent> E toPortalEvent(final String eventData, Class<E> eventType) {
try {
return mapper.readValue(eventData, eventType);
} catch (JsonParseException e) {
throw new RuntimeException("Failed to deserialize PortalEvent data", e);
} catch (JsonMappingException e) {
throw new RuntimeException("Failed to deserialize PortalEvent data", e);
} catch (IOException e) {
throw new RuntimeException("Failed to deserialize PortalEvent data", e);
}
}
protected String toString(PortalEvent event) {
try {
return mapper.writeValueAsString(event);
} catch (JsonParseException e) {
throw new RuntimeException("Failed to serialize PortalEvent data", e);
} catch (JsonMappingException e) {
throw new RuntimeException("Failed to serialize PortalEvent data", e);
} catch (IOException e) {
throw new RuntimeException("Failed to serialize PortalEvent data", e);
}
}
}