/*
* Copyright (C) 2007 - 2014 GeoSolutions S.A.S.
* http://www.geo-solutions.it
*
* GPLv3 + Classpath exception
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.geoserver.geofence.cache;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.geofence.config.GeoFenceConfigurationManager;
import org.geoserver.geofence.services.RuleReaderService;
import org.geoserver.geofence.services.dto.AccessInfo;
import org.geoserver.geofence.services.dto.AuthUser;
import org.geoserver.geofence.services.dto.RuleFilter;
import org.geoserver.geofence.services.dto.ShortRule;
import org.geotools.util.logging.Logging;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheStats;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
/**
* A delegating {@link RuleReaderService} with caching capabilities.
* <P/>
* Cache eviction policy is LRU.<br/>
* Cache coherence is handled by entry timeout.<br/>
* <p/>
*
* @author ETj (etj at geo-solutions.it)
*/
public class CachedRuleReader implements RuleReaderService {
static final Logger LOGGER = Logging.getLogger(CachedRuleReader.class);
private RuleReaderService realRuleReaderService;
private LoadingCache<RuleFilter, AccessInfo> ruleCache;
private LoadingCache<NamePw, AuthUser> userCache;
private final GeoFenceConfigurationManager configurationManager;
/**
* Latest configuration used
*/
private CacheConfiguration cacheConfiguration = new CacheConfiguration();
public CachedRuleReader(GeoFenceConfigurationManager configurationManager) {
this.configurationManager = configurationManager;
// pull config when initializing
init();
}
/**
* (Re)Init the cache, pulling the configuration from the configurationManager.
*
* Please use {@link #getCacheInitParams() } to set the cache parameters before
* <code>init()</code>ting the cache
*/
public final void init() {
cacheConfiguration = configurationManager.getCacheConfiguration();
ruleCache = getCacheBuilder().build(new RuleLoader());
userCache = getCacheBuilder().build(new UserLoader());
}
protected CacheBuilder getCacheBuilder() {
CacheBuilder builder = CacheBuilder.newBuilder()
.maximumSize(cacheConfiguration.getSize())
.refreshAfterWrite(cacheConfiguration.getRefreshMilliSec(), TimeUnit.MILLISECONDS) // reloadable after x time
.expireAfterWrite(cacheConfiguration.getExpireMilliSec(), TimeUnit.MILLISECONDS) // throw away entries too old
.recordStats()
;
//.expireAfterAccess(timeoutMillis, TimeUnit.MILLISECONDS)
// .removalListener(MY_LISTENER)
// this should only be used while testing
if(cacheConfiguration.getCustomTicker() != null) {
LOGGER.log(Level.SEVERE, "Setting a custom Ticker in the cache {0}", cacheConfiguration.getCustomTicker().getClass().getName());
builder.ticker(cacheConfiguration.getCustomTicker());
}
return builder;
}
private class RuleLoader extends CacheLoader<RuleFilter, AccessInfo> {
@Override
public AccessInfo load(RuleFilter filter) throws Exception {
if(LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "Loading {0}", filter);
return realRuleReaderService.getAccessInfo(filter);
}
@Override
public ListenableFuture<AccessInfo> reload(final RuleFilter filter, AccessInfo accessInfo) throws Exception {
if(LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "Reloading {0}", filter);
// this is a sync implementation
AccessInfo ret = realRuleReaderService.getAccessInfo(filter);
return Futures.immediateFuture(ret);
// next there is an asynchronous implementation, but in tests it seems to hang
// return ListenableFutureTask.create(new Callable<AccessInfo>() {
// @Override
// public AccessInfo call() throws Exception {
// if(LOGGER.isLoggable(Level.FINE))
// LOGGER.log(Level.FINE, "Asynch reloading {0}", filter);
// return realRuleReaderService.getAccessInfo(filter);
// }
// });
}
}
private class UserLoader extends CacheLoader<NamePw, AuthUser> {
@Override
public AuthUser load(NamePw user) throws NoAuthException {
if(LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "Loading user '"+user.getName()+"'");
AuthUser auth = realRuleReaderService.authorize(user.getName(), user.getPw());
if(auth==null)
throw new NoAuthException("Can't auth user ["+user.getName()+"]");
return auth;
}
@Override
public ListenableFuture<AuthUser> reload(final NamePw user, AuthUser authUser) throws NoAuthException {
if(LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "Reloading user '"+user.getName()+"'");
// this is a sync implementation
AuthUser auth = realRuleReaderService.authorize(user.getName(), user.getPw());
if(auth==null)
throw new NoAuthException("Can't auth user ["+user.getName()+"]");
return Futures.immediateFuture(auth);
// todo: we may want a asynchronous implementation
}
}
public void invalidateAll() {
if(LOGGER.isLoggable(Level.WARNING))
LOGGER.log(Level.WARNING, "Forcing cache invalidation");
ruleCache.invalidateAll();
userCache.invalidateAll();
}
/**
* <B>Deprecated method are not cached.</B>
*
* @deprecated Use {@link #getAccessInfo(RuleFilter filter) }
*/
@Override
public AccessInfo getAccessInfo(String userName, String profileName, String instanceName, String sourceAddress, String service, String request, String workspace, String layer) {
LOGGER.severe("DEPRECATED METHODS ARE NOT CACHED");
return realRuleReaderService.getAccessInfo(userName, profileName, instanceName, sourceAddress, service, request, workspace, layer);
}
private AtomicLong dumpCnt = new AtomicLong(0);
@Override
public AccessInfo getAccessInfo(RuleFilter filter) {
if(LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "Request for {0}", filter);
if(LOGGER.isLoggable(Level.INFO))
if(dumpCnt.incrementAndGet() % 10 == 0) {
LOGGER.info("Rules :"+ruleCache.stats());
LOGGER.info("Users :"+userCache.stats());
LOGGER.fine("params :"+cacheConfiguration);
}
try {
return ruleCache.get(filter);
} catch (ExecutionException ex) {
throw new RuntimeException(ex); // fixme: handle me
}
}
/**
* <B>Deprecated method are not cached.</B>
*
* @deprecated Use {@link #getMatchingRules(RuleFilter filter) }
*/
@Override
public List<ShortRule> getMatchingRules(String userName, String profileName, String instanceName, String sourceAddress, String service, String request, String workspace, String layer) {
LOGGER.severe("DEPRECATED METHODS ARE NOT CACHED");
return realRuleReaderService.getMatchingRules(userName, profileName, instanceName, sourceAddress, service, request, workspace, layer);
}
@Override
public List<ShortRule> getMatchingRules(RuleFilter filter) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public AuthUser authorize(String username, String password) {
try {
return userCache.get(new NamePw(username, password));
// } catch (NoAuthException ex) {
// LOGGER.warning(ex.getMessage());
// return null;
} catch (ExecutionException ex) {
LOGGER.warning(ex.getMessage());
return null;
}
}
//--------------------------------------------------------------------------
public void setRealRuleReaderService(RuleReaderService realRuleReaderService) {
this.realRuleReaderService = realRuleReaderService;
}
public CacheConfiguration getCacheInitParams() {
return cacheConfiguration;
}
public CacheStats getStats() {
return ruleCache.stats();
}
public CacheStats getUserStats() {
return userCache.stats();
}
public long getCacheSize() {
return ruleCache.size();
}
public long getUserCacheSize() {
return userCache.size();
}
/**
* May be useful if an external peer doesn't want to use the guava dep.
*/
public String getStatsString() {
return ruleCache.stats().toString();
}
@Override
public String toString() {
return getClass().getSimpleName()
+"["
+ "Rule:"+ruleCache.stats()
+ " User:"+userCache.stats()
+ " " + cacheConfiguration
+ "]";
}
protected static class NamePw {
private String name;
private String pw;
public NamePw() {
}
public NamePw(String name, String pw) {
this.name = name;
this.pw = pw;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPw() {
return pw;
}
public void setPw(String pw) {
this.pw = pw;
}
@Override
public int hashCode() {
int hash = 7;
hash = 89 * hash + (this.name != null ? this.name.hashCode() : 0);
hash = 89 * hash + (this.pw != null ? this.pw.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final NamePw other = (NamePw) obj;
if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
return false;
}
if ((this.pw == null) ? (other.pw != null) : !this.pw.equals(other.pw)) {
return false;
}
return true;
}
}
class NoAuthException extends Exception {
public NoAuthException() {
}
public NoAuthException(String message) {
super(message);
}
public NoAuthException(String message, Throwable cause) {
super(message, cause);
}
public NoAuthException(Throwable cause) {
super(cause);
}
}
}