/*
* Copyright 2009 Martin Grotzke
*
* Licensed 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 de.javakaffee.web.msm;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nonnull;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.SuppressWarnings;
/**
* An LRUCache that supports a maximum number of cache entries and a time to
* live for them. The TTL is measured from insertion time to access time.
*
* @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a>
* @version $Id$
* @param <K>
* the type of the key
*/
public class NodeAvailabilityCache<K> {
private static final Log LOG = LogFactory.getLog( NodeAvailabilityCache.class );
private final long _ttl;
private final ConcurrentHashMap<K, ManagedItem<Boolean>> _map;
private final CacheLoader<K> _cacheLoader;
/**
* Create a new LRUCache with a maximum number of cache entries and a
* specified time to live for cache entries. The TTL is measured from
* insertion time to access time.
*
* @param size
* the maximum number of cached items
* @param ttlInMillis
* the time to live in milli seconds. Specify -1 for no limit
* @param cacheLoader
* the cache loader to use
*/
public NodeAvailabilityCache( final int size, final long ttlInMillis, final CacheLoader<K> cacheLoader ) {
_ttl = ttlInMillis;
_map = new ConcurrentHashMap<K, ManagedItem<Boolean>>( size / 2 );
_cacheLoader = cacheLoader;
}
/**
* If the specified key is not already associated with a value or if it's
* associated with a different value, associate it with the given value.
* This is equivalent to
*
* <pre>
* <code> if (map.get(key) == null || !map.get(key).equals(value))
* return map.put(key, value);
* else
* return map.get(key);
* </code>
* </pre>
*
* except that the action is performed atomically.
*
* @param key
* the key to associate the value with.
* @param available
* the value to associate with the provided key.
* @return the previous value associated with the specified key, or null if
* there was no mapping for the key
*/
@CheckForNull
@SuppressWarnings( "NP_BOOLEAN_RETURN_NULL" )
public Boolean setNodeAvailable( final K key, final boolean available ) {
final ManagedItem<Boolean> item = _map.get( key );
final Boolean availableObj = Boolean.valueOf( available );
if ( item == null || item._value != availableObj ) {
final ManagedItem<Boolean> previous =
_map.put( key, new ManagedItem<Boolean>( availableObj, System.currentTimeMillis() ) );
return previous != null
? previous._value
: null;
} else {
return item._value;
}
}
/**
* Determines, if the node is available. If it's not cached, it's loaded
* from the cache loader.
*
* @param key
* the key to check
* @return <code>true</code> if the node is marked as available.
*/
public boolean isNodeAvailable( @Nonnull final K key ) {
final ManagedItem<Boolean> item = _map.get( key );
if ( item == null ) {
return updateIsNodeAvailable( key );
} else if ( isExpired( item ) ) {
_map.remove( key );
return updateIsNodeAvailable( key );
} else {
return item._value;
}
}
private boolean isExpired( final ManagedItem<Boolean> item ) {
return _ttl > -1 && System.currentTimeMillis() - item._insertionTime > _ttl;
}
private boolean updateIsNodeAvailable( final K key ) {
final Boolean result = Boolean.valueOf( _cacheLoader.isNodeAvailable( key ) );
if ( LOG.isDebugEnabled() ) {
LOG.debug( "CacheLoader returned node availability '" + result + "' for node '" + key + "'." );
}
_map.put( key, new ManagedItem<Boolean>( result, System.currentTimeMillis() ) );
return result;
}
/**
* All known keys.
*
* @return a list of all keys, never <code>null</code>.
*/
public List<K> getKeys() {
return new ArrayList<K>( _map.keySet() );
}
/**
* A set of nodes that are stored as unavailable.
*
* @return a set of unavailable nodes, never <code>null</code>.
*/
public Set<K> getUnavailableNodes() {
final Set<K> result = new HashSet<K>();
for ( final Map.Entry<K, ManagedItem<Boolean>> entry : _map.entrySet() ) {
if ( !entry.getValue()._value.booleanValue() && !isExpired( entry.getValue() ) ) {
result.add( entry.getKey() );
}
}
return result;
}
/**
* Stores a value with the timestamp this value was added to the cache.
*
* @param <T>
* the type of the value
*/
private static final class ManagedItem<T> {
private final T _value;
private final long _insertionTime;
private ManagedItem( final T value, final long accessTime ) {
_value = value;
_insertionTime = accessTime;
}
}
/**
* The cache loader interface.
*
* @param <K>
* the type of the key.
*/
static interface CacheLoader<K> {
boolean isNodeAvailable( K key );
}
}