package org.apache.commons.jcs.access;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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
*
* 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.
*/
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.jcs.JCS;
import org.apache.commons.jcs.access.behavior.ICacheAccess;
import org.apache.commons.jcs.access.exception.CacheException;
import org.apache.commons.jcs.access.exception.ConfigurationException;
import org.apache.commons.jcs.engine.behavior.ICacheElement;
import org.apache.commons.jcs.engine.behavior.ICompositeCacheAttributes;
import org.apache.commons.jcs.engine.behavior.IElementAttributes;
import org.apache.commons.jcs.engine.stats.behavior.ICacheStats;
import org.apache.commons.jcs.utils.props.AbstractPropertyContainer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* TODO: Add new methods that will allow you to provide a partition indicator for all major calls. Add an interface as well.
* <p>
* This handles dividing puts and gets.
* <p>
* There are two required properties.
* <p>
* <ol>
* <li>.numberOfPartitions</li>
* <li>.partitionRegionNamePrefix</li>
* </ol>
* System properties will override values in the properties file.
* <p>
* We use a JCS region name for each partition that looks like this: partitionRegionNamePrefix + "_"
* + partitionNumber. The number is 0 indexed based.
* <p>
* @author Aaron Smuts
*/
public class PartitionedCacheAccess<K, V>
extends AbstractPropertyContainer
implements ICacheAccess<K, V>
{
/** the logger. */
private static final Log log = LogFactory.getLog( PartitionedCacheAccess.class );
/** The number of partitions. */
private int numberOfPartitions = 1;
/**
* We use a JCS region name for each partition that looks like this: partitionRegionNamePrefix +
* "_" + partitionNumber
*/
private String partitionRegionNamePrefix;
/** An array of partitions built during initialization. */
private ICacheAccess<K, V>[] partitions;
/** Is the class initialized. */
private boolean initialized = false;
/** Sets default properties heading and group. */
public PartitionedCacheAccess()
{
setPropertiesHeading( "PartitionedCacheAccess" );
setPropertiesGroup( "cache" );
}
/**
* Puts the value into the appropriate cache partition.
* <p>
* @param key key
* @param object object
* @throws CacheException on configuration problem
*/
@Override
public void put( K key, V object )
throws CacheException
{
if ( key == null || object == null )
{
log.warn( "Bad input key [" + key + "]. Cannot put null into the cache." );
return;
}
if (!ensureInit())
{
return;
}
int partition = getPartitionNumberForKey( key );
try
{
partitions[partition].put( key, object );
}
catch ( CacheException e )
{
log.error( "Problem putting value for key [" + key + "] in cache [" + partitions[partition] + "]" );
throw e;
}
}
/**
* Puts in cache if an item does not exist with the name in that region.
* <p>
* @param key
* @param object
* @throws CacheException
*/
@Override
public void putSafe( K key, V object )
throws CacheException
{
if ( key == null || object == null )
{
log.warn( "Bad input key [" + key + "]. Cannot putSafe null into the cache." );
}
if (!ensureInit())
{
return;
}
int partition = getPartitionNumberForKey( key );
partitions[partition].putSafe( key, object );
}
/**
* Puts the value into the appropriate cache partition.
* <p>
* @param key key
* @param object object
* @param attr
* @throws CacheException on configuration problem
*/
@Override
public void put( K key, V object, IElementAttributes attr )
throws CacheException
{
if ( key == null || object == null )
{
log.warn( "Bad input key [" + key + "]. Cannot put null into the cache." );
return;
}
if (!ensureInit())
{
return;
}
int partition = getPartitionNumberForKey( key );
try
{
partitions[partition].put( key, object, attr );
}
catch ( CacheException e )
{
log.error( "Problem putting value for key [" + key + "] in cache [" + partitions[partition] + "]" );
throw e;
}
}
/**
* Gets the object for the key from the desired partition.
* <p>
* @param key key
* @return result, null if not found.
*/
@Override
public V get( K key )
{
if ( key == null )
{
log.warn( "Input key is null." );
return null;
}
if (!ensureInit())
{
return null;
}
int partition = getPartitionNumberForKey( key );
return partitions[partition].get( key );
}
/**
* Gets the ICacheElement<K, V> (the wrapped object) for the key from the desired partition.
* <p>
* @param key key
* @return result, null if not found.
*/
@Override
public ICacheElement<K, V> getCacheElement( K key )
{
if ( key == null )
{
log.warn( "Input key is null." );
return null;
}
if (!ensureInit())
{
return null;
}
int partition = getPartitionNumberForKey( key );
return partitions[partition].getCacheElement( key );
}
/**
* This is a getMultiple. We try to group the keys so that we make as few calls as needed.
* <p>
* @param names
* @return Map of keys to ICacheElement
*/
@Override
public Map<K, ICacheElement<K, V>> getCacheElements( Set<K> names )
{
if ( names == null )
{
log.warn( "Bad input names cannot be null." );
return Collections.emptyMap();
}
if (!ensureInit())
{
return Collections.emptyMap();
}
@SuppressWarnings("unchecked") // No generic arrays in java
Set<K>[] dividedNames = new Set[this.getNumberOfPartitions()];
for (K key : names)
{
int partition = getPartitionNumberForKey( key );
if ( dividedNames[partition] == null )
{
dividedNames[partition] = new HashSet<K>();
}
dividedNames[partition].add( key );
}
Map<K, ICacheElement<K, V>> result = new HashMap<K, ICacheElement<K, V>>();
for ( int i = 0; i < partitions.length; i++ )
{
if ( dividedNames[i] != null && !dividedNames[i].isEmpty() )
{
result.putAll( partitions[i].getCacheElements( dividedNames[i] ) );
}
}
return result;
}
/**
* This is tricky. Do we need to get from all the partitions?
* <p>
* If this interface took an object, we could use the hashcode to determine the partition. Then
* we could use the toString for the pattern.
* <p>
* @param pattern
* @return HashMap key to value
*/
@Override
public Map<K, V> getMatching( String pattern )
{
if ( pattern == null )
{
log.warn( "Input pattern is null." );
return null;
}
if (!ensureInit())
{
return null;
}
Map<K, V> result = new HashMap<K, V>();
for (ICacheAccess<K, V> partition : partitions)
{
result.putAll( partition.getMatching( pattern ) );
}
return result;
}
/**
* This is tricky. Do we need to get from all the partitions?
* <p>
* @param pattern
* @return HashMap key to ICacheElement
*/
@Override
public Map<K, ICacheElement<K, V>> getMatchingCacheElements( String pattern )
{
if ( pattern == null )
{
log.warn( "Input pattern is null." );
return null;
}
if (!ensureInit())
{
return null;
}
Map<K, ICacheElement<K, V>> result = new HashMap<K, ICacheElement<K, V>>();
for (ICacheAccess<K, V> partition : partitions)
{
result.putAll( partition.getMatchingCacheElements( pattern ) );
}
return result;
}
/**
* Removes the item from the appropriate partition.
* <p>
* @param key
* @throws CacheException
*/
@Override
public void remove( K key )
throws CacheException
{
if ( key == null )
{
log.warn( "Input key is null. Cannot remove null from the cache." );
return;
}
if (!ensureInit())
{
return;
}
int partition = getPartitionNumberForKey( key );
try
{
partitions[partition].remove( key );
}
catch ( CacheException e )
{
log.error( "Problem removing value for key [" + key + "] in cache [" + partitions[partition] + "]" );
throw e;
}
}
/**
* Calls free on each partition.
* <p>
* @param numberToFree
* @return number removed
* @throws CacheException
*/
@Override
public int freeMemoryElements( int numberToFree )
throws CacheException
{
if (!ensureInit())
{
return 0;
}
int count = 0;
for (ICacheAccess<K, V> partition : partitions)
{
count += partition.freeMemoryElements( numberToFree );
}
return count;
}
/**
* @return ICompositeCacheAttributes from the first partition.
*/
@Override
public ICompositeCacheAttributes getCacheAttributes()
{
if (!ensureInit())
{
return null;
}
if ( partitions.length == 0 )
{
return null;
}
return partitions[0].getCacheAttributes();
}
/**
* @return IElementAttributes from the first partition.
* @throws CacheException
*/
@Override
public IElementAttributes getDefaultElementAttributes()
throws CacheException
{
if (!ensureInit())
{
return null;
}
if ( partitions.length == 0 )
{
return null;
}
return partitions[0].getDefaultElementAttributes();
}
/**
* This is no more efficient than simply getting the cache element.
* <p>
* @param key
* @return IElementAttributes
* @throws CacheException
*/
@Override
public IElementAttributes getElementAttributes( K key )
throws CacheException
{
if ( key == null )
{
log.warn( "Input key is null. Cannot getElementAttributes for null from the cache." );
return null;
}
if (!ensureInit())
{
return null;
}
int partition = getPartitionNumberForKey( key );
return partitions[partition].getElementAttributes( key );
}
/**
* Resets the attributes for this item. This has the same effect as an update, in most cases.
* None of the auxiliaries are optimized to do this more efficiently than a simply update.
* <p>
* @param key
* @param attributes
* @throws CacheException
*/
@Override
public void resetElementAttributes( K key, IElementAttributes attributes )
throws CacheException
{
if ( key == null )
{
log.warn( "Input key is null. Cannot resetElementAttributes for null." );
return;
}
if (!ensureInit())
{
return;
}
int partition = getPartitionNumberForKey( key );
partitions[partition].resetElementAttributes( key, attributes );
}
/**
* Sets the attributes on all the partitions.
* <p>
* @param cattr
*/
@Override
public void setCacheAttributes( ICompositeCacheAttributes cattr )
{
if (!ensureInit())
{
return;
}
for (ICacheAccess<K, V> partition : partitions)
{
partition.setCacheAttributes( cattr );
}
}
/**
* Removes all of the elements from a region.
* <p>
* @throws CacheException
*/
@Override
public void clear()
throws CacheException
{
if (!ensureInit())
{
return;
}
for (ICacheAccess<K, V> partition : partitions)
{
partition.clear();
}
}
/**
* This method is does not reset the attributes for items already in the cache. It could
* potentially do this for items in memory, and maybe on disk (which would be slow) but not
* remote items. Rather than have unpredictable behavior, this method just sets the default
* attributes. Items subsequently put into the cache will use these defaults if they do not
* specify specific attributes.
* <p>
* @param attr the default attributes.
* @throws CacheException if something goes wrong.
*/
@Override
public void setDefaultElementAttributes( IElementAttributes attr )
throws CacheException
{
if (!ensureInit())
{
return;
}
for (ICacheAccess<K, V> partition : partitions)
{
partition.setDefaultElementAttributes(attr);
}
}
/**
* This returns the ICacheStats object with information on this region and its auxiliaries.
* <p>
* This data can be formatted as needed.
* <p>
* @return ICacheStats
*/
@Override
public ICacheStats getStatistics()
{
if (!ensureInit())
{
return null;
}
if ( partitions.length == 0 )
{
return null;
}
return partitions[0].getStatistics();
}
/**
* @return A String version of the stats.
*/
@Override
public String getStats()
{
if (!ensureInit())
{
return "";
}
StringBuilder stats = new StringBuilder();
for (ICacheAccess<K, V> partition : partitions)
{
stats.append(partition.getStats());
stats.append("\n");
}
return stats.toString();
}
/**
* Dispose this region. Flushes objects to and closes auxiliary caches. This is a shutdown
* command!
* <p>
* To simply remove all elements from the region use clear().
*/
@Override
public synchronized void dispose()
{
if (!ensureInit())
{
return;
}
for (ICacheAccess<K, V> partition : partitions)
{
partition.dispose();
}
initialized = false;
}
/**
* This expects a numeric key. If the key cannot be converted into a number, we will return 0.
* TODO we could md5 it or get the hashcode.
* <p>
* We determine the partition by taking the mod of the number of partitions.
* <p>
* @param key key
* @return the partition number.
*/
protected int getPartitionNumberForKey( K key )
{
if ( key == null )
{
return 0;
}
long keyNum = getNumericValueForKey( key );
int partition = (int) ( keyNum % getNumberOfPartitions() );
if ( log.isDebugEnabled() )
{
log.debug( "Using partition [" + partition + "] for key [" + key + "]" );
}
return partition;
}
/**
* This can be overridden for special purposes.
* <p>
* @param key key
* @return long
*/
public long getNumericValueForKey( K key )
{
String keyString = key.toString();
long keyNum = -1;
try
{
keyNum = Long.parseLong( keyString );
}
catch ( NumberFormatException e )
{
// THIS IS UGLY, but I can't think of a better failsafe right now.
keyNum = key.hashCode();
log.warn( "Couldn't convert [" + key + "] into a number. Will use hashcode [" + keyNum + "]" );
}
return keyNum;
}
/**
* Initialize if we haven't already.
* <p>
* @throws ConfigurationException on configuration problem
*/
protected synchronized boolean ensureInit()
{
if ( !initialized )
{
try
{
initialize();
}
catch ( ConfigurationException e )
{
log.error( "Couldn't configure partioned access.", e );
return false;
}
}
return true;
}
/**
* Use the partition prefix and the number of partitions to get JCS regions.
* <p>
* @throws ConfigurationException on configuration problem
*/
protected synchronized void initialize()
throws ConfigurationException
{
ensureProperties();
@SuppressWarnings("unchecked") // No generic arrays in java
ICacheAccess<K, V>[] tempPartitions = new ICacheAccess[this.getNumberOfPartitions()];
for ( int i = 0; i < this.getNumberOfPartitions(); i++ )
{
String regionName = this.getPartitionRegionNamePrefix() + "_" + i;
try
{
tempPartitions[i] = JCS.getInstance( regionName );
}
catch ( CacheException e )
{
log.error( "Problem getting cache for region [" + regionName + "]" );
}
}
partitions = tempPartitions;
initialized = true;
}
/**
* Loads in the needed configuration settings. System properties are checked first. A system
* property will override local property value.
* <p>
* Loads the following JCS Cache specific properties:
* <ul>
* <li>heading.numberOfPartitions</li>
* <li>heading.partitionRegionNamePrefix</li>
* </ul>
* @throws ConfigurationException on configuration problem
*/
@Override
protected void handleProperties()
throws ConfigurationException
{
// Number of Partitions.
String numberOfPartitionsPropertyName = this.getPropertiesHeading() + ".numberOfPartitions";
String numberOfPartitionsPropertyValue = getPropertyForName( numberOfPartitionsPropertyName, true );
try
{
this.setNumberOfPartitions( Integer.parseInt( numberOfPartitionsPropertyValue ) );
}
catch ( NumberFormatException e )
{
String message = "Could not convert [" + numberOfPartitionsPropertyValue + "] into a number for ["
+ numberOfPartitionsPropertyName + "]";
log.error( message );
throw new ConfigurationException( message );
}
// Partition Name Prefix.
String prefixPropertyName = this.getPropertiesHeading() + ".partitionRegionNamePrefix";
String prefix = getPropertyForName( prefixPropertyName, true );
this.setPartitionRegionNamePrefix( prefix );
}
/**
* Checks the system properties before the properties.
* <p>
* @param propertyName name
* @param required is it required?
* @return the property value if one is found
* @throws ConfigurationException thrown if it is required and not found.
*/
protected String getPropertyForName( String propertyName, boolean required )
throws ConfigurationException
{
String propertyValue = null;
propertyValue = System.getProperty( propertyName );
if ( propertyValue != null )
{
if ( log.isInfoEnabled() )
{
log.info( "Found system property override: Name [" + propertyName + "] Value [" + propertyValue + "]" );
}
}
else
{
propertyValue = this.getProperties().getProperty( propertyName );
if ( required && propertyValue == null )
{
String message = "Could not find required property [" + propertyName + "] in propertiesGroup ["
+ this.getPropertiesGroup() + "]";
log.error( message );
throw new ConfigurationException( message );
}
else
{
if ( log.isInfoEnabled() )
{
log.info( "Name [" + propertyName + "] Value [" + propertyValue + "]" );
}
}
}
return propertyValue;
}
/**
* @param numberOfPartitions The numberOfPartitions to set.
*/
protected void setNumberOfPartitions( int numberOfPartitions )
{
this.numberOfPartitions = numberOfPartitions;
}
/**
* @return Returns the numberOfPartitions.
*/
protected int getNumberOfPartitions()
{
return numberOfPartitions;
}
/**
* @param partitionRegionNamePrefix The partitionRegionNamePrefix to set.
*/
protected void setPartitionRegionNamePrefix( String partitionRegionNamePrefix )
{
this.partitionRegionNamePrefix = partitionRegionNamePrefix;
}
/**
* @return Returns the partitionRegionNamePrefix.
*/
protected String getPartitionRegionNamePrefix()
{
return partitionRegionNamePrefix;
}
/**
* @param partitions The partitions to set.
*/
protected void setPartitions( ICacheAccess<K, V>[] partitions )
{
this.partitions = partitions;
}
/**
* @return Returns the partitions.
*/
protected ICacheAccess<K, V>[] getPartitions()
{
return partitions;
}
}