blob: 7d8326338407e2754f1ee91e7e6d258b374f05c9 [file] [log] [blame]
/*****************************************************************
* 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.
****************************************************************/
package org.apache.cayenne.cache;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.di.BeforeScopeEnd;
import org.apache.cayenne.query.QueryMetadata;
import com.opensymphony.oscache.base.CacheEntry;
import com.opensymphony.oscache.base.NeedsRefreshException;
import com.opensymphony.oscache.general.GeneralCacheAdministrator;
/**
* A {@link QueryCache} implementation based on OpenSymphony OSCache. Query cache
* parameters are initialized from "/oscache.properties" file per <a
* href="https://www.opensymphony.com/oscache/wiki/Configuration.html">OSCache</a>
* documentation. In addition to the standard OSCache parameters, Cayenne provider allows
* to setup global cache expiration parameters, and parameters matching the main query
* cache group (i.e. the cache groups specified first). A sample oscache.properties may
* look like this:
*
* <pre>
* # OSCache configuration file
*
* # OSCache standard configuration per
* # http://www.opensymphony.com/oscache/wiki/Configuration.html
* # ---------------------------------------------------------------
*
* #cache.memory=true
* #cache.blocking=false
* cache.capacity=5000
* cache.algorithm=com.opensymphony.oscache.base.algorithm.LRUCache
*
* # Cayenne specific properties
* # ---------------------------------------------------------------
*
* # Default refresh period in seconds:
* cayenne.default.refresh = 60
*
* # Default expiry specified as cron expressions per
* # http://www.opensymphony.com/oscache/wiki/Cron%20Expressions.html
* # expire entries every hour on the 10's minute
* cayenne.default.cron = 10 * * * *
*
* # Same parameters can be overriden per query
* cayenne.group.xyz.refresh = 120
* cayenne.group.xyz.cron = 10 1 * * *
* </pre>
*
* Further extension of OSQueryCache is possible by using OSCache listener API.
*
* @since 3.0
*/
public class OSQueryCache implements QueryCache {
public static final int DEFAULT_REFRESH_PERIOD = CacheEntry.INDEFINITE_EXPIRY;
static String DEFAULT_REFRESH_KEY = "cayenne.default.refresh";
static String DEFAULT_CRON_KEY = "cayenne.default.cron";
static String GROUP_PREFIX = "cayenne.group.";
static String REFRESH_SUFFIX = ".refresh";
static String CRON_SUFFIX = ".cron";
protected GeneralCacheAdministrator osCache;
RefreshSpecification defaultRefreshSpecification;
Map<String, RefreshSpecification> refreshSpecifications;
Properties properties;
public OSQueryCache() {
OSCacheAdministrator admin = new OSCacheAdministrator();
init(admin, admin.getProperties());
}
public OSQueryCache(GeneralCacheAdministrator cache, Properties properties) {
init(cache, properties);
}
/**
* Returns a collection of group names that have been configured explicitly via
* properties.
*/
@SuppressWarnings("unchecked")
public Collection getGroupNames() {
return refreshSpecifications != null
? Collections.unmodifiableCollection(refreshSpecifications.keySet())
: Collections.EMPTY_SET;
}
public String getCronExpression(String groupName) {
RefreshSpecification spec = null;
if (refreshSpecifications != null) {
spec = refreshSpecifications.get(groupName);
}
if (spec == null) {
spec = defaultRefreshSpecification;
}
return spec.cronExpression;
}
public int getRrefreshPeriod(String groupName) {
RefreshSpecification spec = null;
if (refreshSpecifications != null) {
spec = refreshSpecifications.get(groupName);
}
if (spec == null) {
spec = defaultRefreshSpecification;
}
return spec.refreshPeriod;
}
/**
* Returns the underlying OSCache manager object.
*/
public GeneralCacheAdministrator getOsCache() {
return osCache;
}
/**
* Returns configuration properties. Usually this is the contents of
* "oscache.properties" file.
*/
public Properties getProperties() {
return properties;
}
void init(GeneralCacheAdministrator cache, Properties properties) {
this.properties = properties;
this.osCache = cache;
this.defaultRefreshSpecification = new RefreshSpecification();
// load defaults and per-query settings
if (properties != null) {
// first extract defaults...
String defaultRefresh = properties.getProperty(DEFAULT_REFRESH_KEY);
if (defaultRefresh != null) {
defaultRefreshSpecification.setRefreshPeriod(defaultRefresh);
}
String defaultCron = properties.getProperty(DEFAULT_CRON_KEY);
if (defaultCron != null) {
defaultRefreshSpecification.cronExpression = defaultCron;
}
// now check for per-query settings
for (final Map.Entry<Object, Object> entry : properties.entrySet()) {
if (entry.getKey() == null || entry.getValue() == null) {
continue;
}
String key = entry.getKey().toString();
if (key.startsWith(GROUP_PREFIX)) {
if (key.endsWith(REFRESH_SUFFIX)) {
String name = key.substring(GROUP_PREFIX.length(), key.length()
- REFRESH_SUFFIX.length());
initRefreshPolicy(name, entry.getValue());
}
else if (key.endsWith(CRON_SUFFIX)) {
String name = key.substring(GROUP_PREFIX.length(), key.length()
- CRON_SUFFIX.length());
initCronPolicy(name, entry.getValue());
}
}
}
}
}
/**
* Called internally for each group that is configured with cron policy in the
* properties. Exposed mainly for the benefit of subclasses. When overriding, call
* 'super'.
*/
protected void initCronPolicy(String groupName, Object value) {
nonNullSpec(groupName).cronExpression = value != null ? value.toString() : null;
}
/**
* Called internally for each group that is configured with refresh policy in the
* properties. Exposed mainly for the benefit of subclasses. When overriding, call
* 'super'.
*/
protected void initRefreshPolicy(String groupName, Object value) {
nonNullSpec(groupName).setRefreshPeriod(value);
}
private RefreshSpecification nonNullSpec(String name) {
if (refreshSpecifications == null) {
refreshSpecifications = new HashMap<String, RefreshSpecification>();
}
RefreshSpecification spec = refreshSpecifications.get(name);
if (spec == null) {
spec = new RefreshSpecification();
spec.cronExpression = defaultRefreshSpecification.cronExpression;
spec.refreshPeriod = defaultRefreshSpecification.refreshPeriod;
refreshSpecifications.put(name, spec);
}
return spec;
}
@SuppressWarnings("unchecked")
public List get(QueryMetadata metadata) {
String key = metadata.getCacheKey();
if (key == null) {
return null;
}
RefreshSpecification refresh = getRefreshSpecification(metadata);
try {
return (List) osCache.getFromCache(
key,
refresh.refreshPeriod,
refresh.cronExpression);
}
catch (NeedsRefreshException e) {
osCache.cancelUpdate(key);
return null;
}
}
/**
* Returns a non-null cached value. If it is not present in the cache, it is obtained
* by calling {@link QueryCacheEntryFactory#createObject()}. Whether the cache
* provider will block on the entry update or not is controlled by "cache.blocking"
* configuration property and is "false" by default.
*/
@SuppressWarnings("unchecked")
public List get(QueryMetadata metadata, QueryCacheEntryFactory factory) {
String key = metadata.getCacheKey();
if (key == null) {
return null;
}
RefreshSpecification refresh = getRefreshSpecification(metadata);
try {
return (List) osCache.getFromCache(
key,
refresh.refreshPeriod,
refresh.cronExpression);
}
catch (NeedsRefreshException e) {
boolean updated = false;
try {
Object result = factory.createObject();
if (!(result instanceof List)) {
if (result == null) {
throw new CayenneRuntimeException("Null on cache rebuilding: "
+ metadata.getCacheKey());
}
else {
throw new CayenneRuntimeException(
"Invalid query result, expected List, got "
+ result.getClass().getName());
}
}
List list = (List) result;
put(metadata, list);
updated = true;
return list;
}
finally {
if (!updated) {
// It is essential that cancelUpdate is called if the
// cached content could not be rebuilt
osCache.cancelUpdate(key);
}
}
}
}
/**
* Returns non-null RefreshSpecification for the QueryMetadata.
*/
RefreshSpecification getRefreshSpecification(QueryMetadata metadata) {
RefreshSpecification refresh = null;
if (refreshSpecifications != null) {
String[] groups = metadata.getCacheGroups();
if (groups != null && groups.length > 0) {
refresh = refreshSpecifications.get(groups[0]);
}
}
return refresh != null ? refresh : defaultRefreshSpecification;
}
@SuppressWarnings("unchecked")
public void put(QueryMetadata metadata, List results) {
String key = metadata.getCacheKey();
if (key != null) {
osCache.putInCache(key, results, metadata.getCacheGroups());
}
}
public void remove(String key) {
if (key != null) {
osCache.removeEntry(key);
}
}
public void removeGroup(String groupKey) {
if (groupKey != null) {
osCache.flushGroup(groupKey);
}
}
public void clear() {
osCache.flushAll();
}
public int size() {
return osCache.getCache().getSize();
}
public int capacity() {
return osCache.getCache().getCapacity();
}
/**
* Shuts down EhCache CacheManager
*/
@BeforeScopeEnd
public void shutdown() {
osCache.destroy();
}
final static class RefreshSpecification {
int refreshPeriod;
String cronExpression;
RefreshSpecification() {
this.refreshPeriod = DEFAULT_REFRESH_PERIOD;
}
RefreshSpecification(int refrehsPeriod, String cronExpression) {
this.refreshPeriod = refrehsPeriod;
this.cronExpression = cronExpression;
}
void setRefreshPeriod(Object value) {
try {
refreshPeriod = Integer.parseInt(value.toString());
}
catch (NumberFormatException e) {
// ignore...
}
}
}
final static class OSCacheAdministrator extends GeneralCacheAdministrator {
OSCacheAdministrator() {
}
OSCacheAdministrator(Properties properties) {
super(properties);
}
Properties getProperties() {
return super.config.getProperties();
}
}
}