package er.extensions.foundation;
import java.util.UUID;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.foundation.NSKeyValueCodingAdditions;
import er.extensions.appserver.ERXResponseRewriter;
import er.extensions.appserver.ERXWOContext;
/**
* ERXLazyValue provides a way to model lazy-loaded values that invalidate with
* different methods. This is very useful for storing expensive values that are
* returned from bound component methods as well as values that are cached that
* are influenced by different areas of a single page (i.e. ajax operations on
* one part of the page that need to invalidate a cache on another part of the
* page).
*
* @author mschrag
*
* @param <T>
* the type of the lazy value
*/
public class ERXLazyValue<T> {
private static final Logger log = LoggerFactory.getLogger(ERXLazyValue.class);
private ERXLazyValue.Source<T> _dataSource;
private ERXLazyValue.Invalidator _invalidator;
private boolean _valueCached;
private T _value;
/**
* Constructs a new ERXLazyValue with a data source and a NeverInvalidator,
* which behaves like a "once" lazy value.
*
* @param dataSource
* the data source for the lazy value
*/
public ERXLazyValue(ERXLazyValue.Source<T> dataSource) {
this(dataSource, new ERXLazyValue.NeverInvalidator());
}
/**
* Constructs a new ERXLazyValue with a shortcut for a KVCSource and a
* NeverInvalidator, which behaves like a "once" lazy value.
*
* @param target
* the target of the KVCSource
* @param keyPath
* the keypath of the KVCSource
*/
public ERXLazyValue(Object target, String keyPath) {
this(new KVCSource<T>(target, keyPath), new ERXLazyValue.NeverInvalidator());
}
/**
* Constructs a new ERXLazyValue with a shortcut for a KVCSource and an
* invalidator.
*
* @param target
* the target of the KVCSource
* @param keyPath
* the keypath of the KVCSource
* @param invalidator
* the invalidator to use
*/
public ERXLazyValue(Object target, String keyPath, ERXLazyValue.Invalidator invalidator) {
this(new KVCSource<T>(target, keyPath), invalidator);
}
/**
* Constructs a new ERXLazyValue with a data source and an invalidator.
*
* @param dataSource
* the data source for the lazy value
* @param invalidator
* the invalidator to use
*/
public ERXLazyValue(ERXLazyValue.Source<T> dataSource, ERXLazyValue.Invalidator invalidator) {
_dataSource = dataSource;
_invalidator = invalidator;
}
/**
* Forcefully invalidates the lazy value, regardless of the state of the
* invalidator.
*/
public synchronized void invalidate() {
_valueCached = false;
_value = null;
}
/**
* Returns the backing value for this lazy value, which will only sometimes
* trigger a call through to the data source.
*
* @return the backing value
*/
public synchronized T value() {
if (!_valueCached || _invalidator.shouldInvalidate()) {
log.debug("Fetching from {} with invalidator {} ...", _dataSource, _invalidator);
_value = _dataSource.value();
_invalidator.fetchedValue(_value);
_valueCached = true;
}
return _value;
}
/**
* Sets the backging value for this lazy value, which will always call
* through to the underlying data source. This will also cache the new value
* and notify the invalidator of a new fetched value (acting just like if
* value() was called on an invalidated cache).
*
* @param value
* the new value
*/
public synchronized void setValue(T value) {
_value = value;
_valueCached = true;
_dataSource.setValue(value);
_invalidator.fetchedValue(value);
}
/**
* A source provides the undelying value (which is usually uncached) to a
* lazy value.
*
* @author mschrag
*
* @param <T>
* the type of the value
*/
public static interface Source<T> {
/**
* Returns the underlying value.
*
* @return the underlying value
*/
public T value();
/**
* Sets the underlying value.
*
* @param value
* the new value
*/
public void setValue(T value);
}
/**
* ConstantSource provides a Source implementation on top of a fixed value.
*
* @author mschrag
*
* @param <T>
* the type of the value
*/
public static class ConstantSource<T> implements ERXLazyValue.Source<T> {
private T _value;
/**
* Constructs a ConstantSource with a fixed value.
*
* @param value
* the value
*/
public ConstantSource(T value) {
_value = value;
}
/**
* Returns the fixed value.
*
* @return the fixed value
*/
public T value() {
return _value;
}
/**
* Sets a new fixed value.
*
* @param value
* the new value
*/
public void setValue(T value) {
_value = value;
}
@Override
public String toString() {
return "[ConstantSource: value=" + _value + "]";
}
}
/**
* KVCSource provides a wrapper around a KVC binding, which is very useful
* in components. As an example, you might have your people() method return
* a lazy value that is bound to a KVCSource<Person>(this,
* "uncachedPeople").
*
* @author mschrag
*
* @param <T>
* the type of the value
*/
public static class KVCSource<T> implements ERXLazyValue.Source<T> {
private Object _target;
private String _keyPath;
/**
* Constructs a new KVCSource.
*
* @param target
* the target of the KVC binding
* @param keyPath
* the keypath to return a value from
*/
public KVCSource(Object target, String keyPath) {
_target = target;
_keyPath = keyPath;
}
/**
* Returns the value of the kaypath on the target.
*
* @return the value
*/
public T value() {
return (T) NSKeyValueCodingAdditions.DefaultImplementation.valueForKeyPath(_target, _keyPath);
}
/**
* Sets the value of the keypath on the target.
*
* @param value
* the new value
*/
public void setValue(T value) {
NSKeyValueCodingAdditions.DefaultImplementation.takeValueForKeyPath(_target, value, _keyPath);
}
@Override
public String toString() {
return "[KVCSource: target=" + _target.getClass().getSimpleName() + "; keyPath=" + _keyPath + "]";
}
}
/**
* Invalidator provides an mechanism for selectively invalidating the cached
* lazy value.
*
* @author mschrag
*/
public static interface Invalidator {
/**
* Called when the lazy value is refetched from the source.
*
* @param value
* the new value
*/
public void fetchedValue(Object value);
/**
* Returns whether or not the lazy value should invalidate its cache.
*
* @return whether or not the lazy value should invalidate its cache
*/
public boolean shouldInvalidate();
}
/**
* TimedInvalidator specifies that the cached value should be invalidated
* after a specified duration. When the value is refetched, the timer is
* restarted.
*
* @author mschrag
*/
public static class TimedInvalidator implements ERXLazyValue.Invalidator {
private long _timeToLiveInMillis;
private long _cacheTime;
/**
* Constructs a new TimedInvalidator.
*
* @param timeToLiveInMillis
* the time-to-live in milliseconds
*/
public TimedInvalidator(long timeToLiveInMillis) {
_timeToLiveInMillis = timeToLiveInMillis;
_cacheTime = -1;
}
public void fetchedValue(Object value) {
_cacheTime = System.currentTimeMillis();
}
public boolean shouldInvalidate() {
return _cacheTime == -1 || (System.currentTimeMillis() - _cacheTime) > _timeToLiveInMillis;
}
}
/**
* Returns true from shouldInvalidate, causing the cache to always refresh.
*
* @author mschrag
*/
public static class AlwaysInvalidator implements ERXLazyValue.Invalidator {
public void fetchedValue(Object value) {
// DO NOTHING
}
public boolean shouldInvalidate() {
return true;
}
}
/**
* Returns false from shouldInvalidate, causing the cache to never refresh.
*
* @author mschrag
*/
public static class NeverInvalidator implements ERXLazyValue.Invalidator {
public void fetchedValue(Object value) {
// DO NOTHING
}
public boolean shouldInvalidate() {
return false;
}
}
/**
* The base class for any invalidator that is triggered by the change in a
* cache key.
*
* @author mschrag
*/
public static abstract class CacheKeyInvalidator implements ERXLazyValue.Invalidator {
private Object _lastCacheKey;
public void fetchedValue(Object value) {
_lastCacheKey = cacheKey();
}
/**
* Returns the current value of the cache key.
*
* @return the current value of the cache key
*/
protected abstract Object cacheKey();
public boolean shouldInvalidate() {
Object currentCacheKey = cacheKey();
return ObjectUtils.notEqual(_lastCacheKey, currentCacheKey);
}
}
/**
* The base class for any invalidator that is triggered by the change in a
* cache key with support for changing the value.
*
* @author mschrag
*/
public static abstract class MutableCacheKeyInvalidator extends ERXLazyValue.CacheKeyInvalidator {
/**
* Sets the current value of the cache key.
*
* @param value
* the current value of the cache key
*/
protected abstract void setCacheKey(Object value);
/**
* Sets the current value of the cache key to be a randomly generated
* UUID.
*/
public void uuid() {
setCacheKey(UUID.randomUUID());
}
/**
* Sets the current value of the cache key to be
* System.currentTimeMillis.
*/
public void timestamp() {
setCacheKey(Long.valueOf(System.currentTimeMillis()));
}
}
/**
* ThreadStorageCacheKeyInvalidator triggers a cache invalidation when the
* value of the specified key changes in the ERXThreadStorage.
*
* @author mschrag
*/
public static class ThreadStorageCacheKeyInvalidator extends ERXLazyValue.MutableCacheKeyInvalidator {
private String _key;
public ThreadStorageCacheKeyInvalidator(String key) {
_key = key;
}
@Override
protected Object cacheKey() {
return ERXThreadStorage.valueForKey(_key);
}
@Override
public void setCacheKey(Object value) {
ERXThreadStorage.takeValueForKey(value, _key);
}
}
/**
* PageUserInfoCacheKeyInvalidator triggers a cache invalidation when the
* value of the specified key changes in the ERXResponseRewriter's
* pageUserInfo. This is useful for triggering cache refreshes based on ajax
* updates to other parts of the page.
*
* @author mschrag
*/
public static class PageUserInfoCacheKeyInvalidator extends ERXLazyValue.MutableCacheKeyInvalidator {
private String _key;
public PageUserInfoCacheKeyInvalidator(String key) {
_key = key;
}
@Override
protected Object cacheKey() {
return ERXResponseRewriter.pageUserInfo(ERXWOContext.currentContext()).objectForKey(_key);
}
@Override
public void setCacheKey(Object value) {
ERXResponseRewriter.pageUserInfo(ERXWOContext.currentContext()).setObjectForKey(value, _key);
}
}
/**
* AjaxPageUserInfoCacheKeyInvalidator triggers a cache invalidation when
* the value of the specified key changes in the ERXResponseRewriter's
* ajaxPageUserInfo. This is similar to PageUserInfoCacheKeyInvalidator
* except that it uses the ajaxPageUserInfo, so the underlying value
* survives across multiple ajax requests to the same page.
*
* @author mschrag
*/
public static class AjaxPageUserInfoCacheKeyInvalidator extends ERXLazyValue.MutableCacheKeyInvalidator {
private String _key;
public AjaxPageUserInfoCacheKeyInvalidator(String key) {
_key = key;
}
@Override
protected Object cacheKey() {
return ERXResponseRewriter.ajaxPageUserInfo(ERXWOContext.currentContext()).objectForKey(_key);
}
@Override
public void setCacheKey(Object value) {
ERXResponseRewriter.ajaxPageUserInfo(ERXWOContext.currentContext()).setObjectForKey(value, _key);
}
}
}