
org.openbase.jul.pattern.AbstractObservable Maven / Gradle / Ivy
package org.openbase.jul.pattern;
/*
* #%L
* JUL Pattern Default
* %%
* Copyright (C) 2015 - 2017 openbase.org
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser 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 General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* .
* #L%
*/
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import org.openbase.jul.exception.CouldNotPerformException;
import org.openbase.jul.exception.FatalImplementationErrorException;
import org.openbase.jul.exception.MultiException;
import org.openbase.jul.exception.MultiException.ExceptionStack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Divine Threepwood
* @param the data type on whose changes is notified
*/
public abstract class AbstractObservable implements Observable {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractObservable.class);
private static final boolean DEFAULT_UNCHANGED_VALUE_FILTER = true;
private static final Object DEFAULT_SOURCE = null;
protected final boolean unchangedValueFilter;
private boolean notificationInProgess;
protected final Object NOTIFICATION_LOCK = new Object() {
@Override
public String toString() {
return "ObservableNotificationLock";
}
};
private final Object OBSERVER_LOCK = new Object() {
@Override
public String toString() {
return "ObserverLock";
}
};
private final Object NOTIFICATION_METHOD_LOCK = new Object() {
@Override
public String toString() {
return "notifyObserverMethodLock";
}
};
protected final List> observers;
protected int latestValueHash;
private Object source;
private ExecutorService executorService;
private HashGenerator hashGenerator;
/**
* Construct new Observable.
*/
public AbstractObservable() {
this(DEFAULT_UNCHANGED_VALUE_FILTER, DEFAULT_SOURCE);
}
/**
* Construct new Observable.
*
* @param source the responsible source of the value notifications.
*/
public AbstractObservable(final Object source) {
this(DEFAULT_UNCHANGED_VALUE_FILTER, source);
}
/**
* Construct new Observable
*
* @param unchangedValueFilter defines if the observer should be informed even if the value is
* the same than notified before.
*/
public AbstractObservable(final boolean unchangedValueFilter) {
this(unchangedValueFilter, DEFAULT_SOURCE);
}
/**
* Construct new Observable.
*
* If the source is not defined the observable itself will be used as notification source.
*
* @param unchangedValueFilter defines if the observer should be informed even if the value is
* the same than notified before.
* @param source the responsible source of the value notifications.
*/
public AbstractObservable(final boolean unchangedValueFilter, final Object source) {
this.observers = new ArrayList<>();
this.unchangedValueFilter = unchangedValueFilter;
this.notificationInProgess = false;
this.source = source == DEFAULT_SOURCE ? this : source; // use observer itself if source was not explicit defined.
this.hashGenerator = new HashGenerator() {
@Override
public int computeHash(T value) throws CouldNotPerformException {
try {
return value.hashCode();
} catch (ConcurrentModificationException ex) {
throw new FatalImplementationErrorException("Observable has changed during hash computation in notification! Set a HashGenerator for the observable to control the hash computation yourself!", this, ex);
}
}
};
}
/**
* {@inheritDoc}
*
* @param observer
*/
@Override
public void addObserver(Observer observer) {
synchronized (OBSERVER_LOCK) {
if (observers.contains(observer)) {
LOGGER.warn("Skip observer registration. Observer[" + observer + "] is already registered!");
return;
}
observers.add(observer);
}
}
/**
* {@inheritDoc}
*
* @param observer
*/
@Override
public void removeObserver(Observer observer) {
synchronized (OBSERVER_LOCK) {
observers.remove(observer);
}
}
/**
* {@inheritDoc}
*/
@Override
public void shutdown() {
synchronized (OBSERVER_LOCK) {
observers.clear();
}
}
/**
* Notify all changes of the observable to all observers only if the observable has changed. The
* source of the notification is set as this. Because of data encapsulation reasons this method
* is not included within the Observer interface.
* Attention! This method is not thread safe against changes of the observable because the check if the observable has changed is
* done by computing its hash value. Therefore if the observable is a collection and it is changed
* while notifying a concurrent modification exception can occur. To avoid this compute the
* observable hash yourself by setting a hash generator.
* If this method is interrupted a rollback is done by reseting the latestHashValue. Thus the observable
* has not changed and false is returned.
*
* @param observable the value which is notified
* @return true if the observable has changed
* @throws MultiException thrown if the notification to at least one observer fails
* @throws CouldNotPerformException thrown if the hash computation fails
*/
public boolean notifyObservers(final T observable) throws MultiException, CouldNotPerformException {
return notifyObservers(this, observable);
}
/**
* Notify all changes of the observable to all observers only if the observable has changed.
* Because of data encapsulation reasons this method is not included within the Observer
* interface.
* Attention! This method is not thread safe against changes of the observable because the check if the observable has changed is
* done by computing its hash value. Therefore if the observable is a collection and it is changed
* while notifying a concurrent modification exception can occur. To avoid this compute the
* observable hash yourself by setting a hash generator.
* If this method is interrupted a rollback is done by reseting the latestHashValue. Thus the observable
* has not changed and false is returned.
*
* Note: In case the given observable is null this notification will be ignored.
*
* @param source the source of the notification
* @param observable the value which is notified
* @return true if the observable has changed
* @throws MultiException thrown if the notification to at least one observer fails
* @throws CouldNotPerformException thrown if the hash computation fails
*/
public boolean notifyObservers(final Observable source, final T observable) throws MultiException, CouldNotPerformException {
synchronized (NOTIFICATION_METHOD_LOCK) {
if (observable == null) {
LOGGER.debug("Skip notification because observable is null!");
return false;
}
ExceptionStack exceptionStack = null;
final Map, Future> notificationFutureList = new HashMap<>();
final ArrayList> tempObserverList;
try {
notificationInProgess = true;
final int observableHash = hashGenerator.computeHash(observable);
if (unchangedValueFilter && isValueAvailable() && observableHash == latestValueHash) {
LOGGER.debug("Skip notification because " + this + " has not been changed!");
return false;
}
applyValueUpdate(observable);
final int lastHashValue = latestValueHash;
latestValueHash = observableHash;
synchronized (OBSERVER_LOCK) {
tempObserverList = new ArrayList<>(observers);
}
for (final Observer observer : tempObserverList) {
if (executorService == null) {
if (Thread.currentThread().isInterrupted()) {
latestValueHash = lastHashValue;
return false;
}
// synchronous notification
try {
observer.update(source, observable);
} catch (InterruptedException ex) {
latestValueHash = lastHashValue;
Thread.currentThread().interrupt();
return false;
} catch (Exception ex) {
exceptionStack = MultiException.push(observer, ex, exceptionStack);
}
} else {
// asynchronous notification
notificationFutureList.put(observer, executorService.submit(new Callable() {
@Override
public Void call() throws Exception {
observer.update(source, observable);
return null;
}
}));
}
}
} finally {
assert observable != null;
notificationInProgess = false;
synchronized (NOTIFICATION_LOCK) {
NOTIFICATION_LOCK.notifyAll();
}
}
//TODO: this check is wrong -> != but when implemented correctly leads bco not starting
// handle exeception printing for async variant
if (executorService == null) {
for (final Entry, Future> notificationFuture : notificationFutureList.entrySet()) {
try {
notificationFuture.getValue().get();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return true;
} catch (Exception ex) {
exceptionStack = MultiException.push(notificationFuture.getKey(), ex, exceptionStack);
}
}
}
MultiException.checkAndThrow("Could not notify Data[" + observable + "] to all observer!", exceptionStack);
return true;
}
}
/**
* Method is called if a observer notification delivers a new value.
*
* @param value the new value
*
* Note: Overwrite this method for getting informed about value changes.
*/
protected void applyValueUpdate(final T value) {
// overwrite for current state holding obervable implementations.
}
/**
* Set an executor service for the observable. If it is set the notification
* will be parallelized using this service.
*
* @param executorService the executor service which will be used for parallelization
*/
public void setExecutorService(ExecutorService executorService) {
this.executorService = executorService;
}
public void setHashGenerator(HashGenerator hashGenerator) {
this.hashGenerator = hashGenerator;
}
/**
* Method checks if a notification is currently in progess.
*
* @return notificationInProgess returns true if a notification is currently in progess.
*/
public boolean isNotificationInProgess() {
return notificationInProgess;
}
@Override
public String toString() {
return Observable.class.getSimpleName() + "[" + (source == this ? source.getClass().getSimpleName() : source) + "]";
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy