/*
* This file is part of LibrePlan
*
* Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e
* Desenvolvemento Tecnolóxico de Galicia
* Copyright (C) 2010-2011 Igalia, S.L.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.libreplan.business.hibernate.notification;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import javax.annotation.PostConstruct;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.AbstractEvent;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.PostDeleteEvent;
import org.hibernate.event.spi.PostDeleteEventListener;
import org.hibernate.event.spi.PostInsertEvent;
import org.hibernate.event.spi.PostInsertEventListener;
import org.hibernate.event.spi.PostUpdateEvent;
import org.hibernate.event.spi.PostUpdateEventListener;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Óscar González Fernández
*/
@Component
public class HibernateDatabaseModificationsListener implements
PostInsertEventListener,
PostUpdateEventListener,
PostDeleteEventListener,
ISnapshotRefresherService {
private static final Log LOG = LogFactory.getLog(HibernateDatabaseModificationsListener.class);
private final ExecutorService executor = Executors.newFixedThreadPool(3);
private final ConcurrentMap<Class<?>, BlockingQueue<NotBlockingAutoUpdatedSnapshot<?>>> interested;
private ConcurrentMap<Transaction, Dispatcher> pending = new ConcurrentHashMap<>();
private Set<NotBlockingAutoUpdatedSnapshot<?>> snapshotsInterestedOn(Class<?> entityClass) {
List<Class<?>> list = new ArrayList<>(1);
list.add(entityClass);
return snapshotsInterestedOn(list);
}
private Set<NotBlockingAutoUpdatedSnapshot<?>> snapshotsInterestedOn(Collection<? extends Class<?>> classesList) {
Set<NotBlockingAutoUpdatedSnapshot<?>> result = new HashSet<>();
for (Class<?> each : new HashSet<>(classesList)) {
BlockingQueue<NotBlockingAutoUpdatedSnapshot<?>> queue = interested.get(each);
if ( queue != null ) {
result.addAll(queue);
}
}
return result;
}
private final class Dispatcher implements Synchronization {
private BlockingQueue<Class<?>> classes = new LinkedBlockingQueue<>();
private final Transaction transaction;
public Dispatcher(Transaction transaction, Class<?> entityClass) {
classes.offer(entityClass);
this.transaction = transaction;
}
public void add(Class<?> entityClass) {
classes.offer(entityClass);
}
@Override
public void beforeCompletion() {
}
@Override
public void afterCompletion(int status) {
LOG.debug("transaction completed with status: " + status);
pending.remove(transaction);
if ( isProbablySucessful(status) ) {
List<Class<?>> list = new ArrayList<>();
classes.drainTo(list);
LOG.debug(list.size() + " modification events recorded");
Set<NotBlockingAutoUpdatedSnapshot<?>> toDispatch = snapshotsInterestedOn(list);
LOG.debug(
"dispatching " + toDispatch + " snapshots to reload due to transaction successful completion");
dispatch(toDispatch);
}
}
private boolean isProbablySucessful(int status) {
return status != Status.STATUS_ROLLEDBACK && status != Status.STATUS_ROLLING_BACK;
}
}
@Autowired
private SessionFactory sessionFactory;
private volatile boolean hibernateListenersRegistered = false;
public HibernateDatabaseModificationsListener() {
interested = new ConcurrentHashMap<>();
}
@PostConstruct
private void registerHibernateListeners() {
SessionFactoryImpl impl = (SessionFactoryImpl) sessionFactory;
EventListenerRegistry registry = impl.getServiceRegistry().getService(EventListenerRegistry.class);
registry.appendListeners(EventType.POST_INSERT, this);
registry.appendListeners(EventType.POST_UPDATE, this);
registry.appendListeners(EventType.POST_DELETE, this);
hibernateListenersRegistered = true;
}
@Override
public boolean requiresPostCommitHanding(EntityPersister persister) {
return false;
}
@Override
public void onPostDelete(PostDeleteEvent event) {
modificationOn(inferTransaction(event), inferEntityClass(getEntityObject(event)));
}
@Override
public void onPostUpdate(PostUpdateEvent event) {
modificationOn(inferTransaction(event), inferEntityClass(getEntityObject(event)));
}
@Override
public void onPostInsert(PostInsertEvent event) {
modificationOn(inferTransaction(event), inferEntityClass(getEntityObject(event)));
}
private Transaction inferTransaction(AbstractEvent event) {
return event.getSession().getTransaction();
}
private Object getEntityObject(PostInsertEvent event) {
return event.getEntity();
}
private static Object getEntityObject(PostDeleteEvent event) {
return event.getEntity();
}
private static Object getEntityObject(PostUpdateEvent event) {
return event.getEntity();
}
private static Class<?> inferEntityClass(Object entity) {
if ( entity instanceof HibernateProxy ) {
HibernateProxy proxy = (HibernateProxy) entity;
return proxy.getHibernateLazyInitializer().getPersistentClass();
}
return entity.getClass();
}
void modificationOn(Transaction transaction, Class<?> entityClass) {
if ( transaction == null ) {
dispatch(snapshotsInterestedOn(entityClass));
return;
}
Dispatcher newDispatcher = new Dispatcher(transaction, entityClass);
Dispatcher previous;
previous = pending.putIfAbsent(transaction, newDispatcher);
boolean dispatcherAlreadyExisted = previous != null;
if ( dispatcherAlreadyExisted ) {
previous.add(entityClass);
} else {
transaction.registerSynchronization(newDispatcher);
}
}
private void dispatch(Set<NotBlockingAutoUpdatedSnapshot<?>> toBeDispatched) {
toBeDispatched.forEach(this::dispatch);
}
private void dispatch(NotBlockingAutoUpdatedSnapshot<?> each) {
each.reloadNeeded(executor);
}
@Override
public <T> IAutoUpdatedSnapshot<T> takeSnapshot(String name, Callable<T> callable, ReloadOn reloadOn) {
if ( !hibernateListenersRegistered ) {
throw new IllegalStateException(
"The hibernate listeners has not been registered. There is some configuration problem.");
}
final NotBlockingAutoUpdatedSnapshot<T> result;
result = new NotBlockingAutoUpdatedSnapshot<>(name, callable);
for (Class<?> each : reloadOn.getClassesOnWhichToReload()) {
interested.putIfAbsent(each, emptyQueue());
BlockingQueue<NotBlockingAutoUpdatedSnapshot<?>> queue = interested.get(each);
boolean success = queue.add(result);
assert success : "the type of queue used must not have restricted capacity";
}
result.ensureFirstLoad(executor);
return result;
}
private BlockingQueue<NotBlockingAutoUpdatedSnapshot<?>> emptyQueue() {
return new LinkedBlockingQueue<>();
}
}