/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * 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 the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>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.apereo.portal.utils.cache.resource; import java.io.IOException; import java.io.Serializable; import java.util.Arrays; import java.util.Map; import java.util.concurrent.TimeUnit; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import net.sf.ehcache.constructs.blocking.SelfPopulatingCache; import org.apereo.portal.utils.cache.ThreadLocalCacheEntryFactory; import org.jasig.resourceserver.aggr.om.Included; import org.jasig.resourceserver.utils.aggr.ResourcesElementsProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; /** * Uses an {@link Ehcache} to handle caching of the resources. * */ @Service public class CachingResourceLoaderImpl implements CachingResourceLoader { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); private final CachedResourceEntryFactory entryFactory = new CachedResourceEntryFactory(); private long checkInterval = TimeUnit.MINUTES.toMillis(1); private Ehcache resourceCache; private ResourcesElementsProvider resourcesElementsProvider; @Autowired public void setResourceCache( @Qualifier("org.apereo.portal.utils.cache.resource.CachingResourceLoader") Ehcache resourceCache) { this.resourceCache = new SelfPopulatingCache(resourceCache, this.entryFactory); } @Autowired public void setResourcesElementsProvider(ResourcesElementsProvider resourcesElementsProvider) { this.resourcesElementsProvider = resourcesElementsProvider; } /** How frequently the resource should be checked for updates (in ms). Defaults to 1 minute. */ public void setCheckInterval(long checkInterval) { this.checkInterval = checkInterval; } /* (non-Javadoc) * @see org.apereo.portal.utils.cache.CachingResourceLoader#getResource(org.springframework.core.io.Resource, org.apereo.portal.utils.cache.ResourceBuilder) */ @Override public <T> CachedResource<T> getResource(Resource resource, Loader<T> builder) throws IOException { return this.getResource(resource, builder, this.checkInterval); } /* (non-Javadoc) * @see org.apereo.portal.utils.cache.CachingResourceLoader#getResource(org.springframework.core.io.Resource, org.apereo.portal.utils.cache.ResourceBuilder, org.apereo.portal.utils.cache.ResourceLoaderOptions) */ @Override public <T> CachedResource<T> getResource( Resource resource, Loader<T> builder, long checkInterval) throws IOException { if (Included.PLAIN == this.resourcesElementsProvider.getDefaultIncludedType()) { this.logger.trace( "Resoure Aggregation Disabled, ignoring resource cache and loading '" + resource + "' directly"); return this.loadResource(resource, builder); } //Look for the resource in the cache, since it has been wrapped with a SelfPopulatingCache it should never return null. final GetResourceArguments<T> arguments = new GetResourceArguments<T>(resource, builder); final Element element = this.entryFactory.getWithData(this.resourceCache, resource, arguments); CachedResource<T> cachedResource = (CachedResource<T>) element.getObjectValue(); if (this.logger.isTraceEnabled()) { this.logger.trace("Found " + cachedResource + " in cache"); } //Found it, now check if the last-load time is within the check interval final long lastCheckTime = cachedResource.getLastCheckTime(); if (lastCheckTime + checkInterval >= System.currentTimeMillis()) { if (this.logger.isTraceEnabled()) { this.logger.trace( cachedResource + " is within checkInterval " + checkInterval + ", returning"); } return cachedResource; } if (this.logger.isTraceEnabled()) { this.logger.trace( cachedResource + " is older than checkInterval " + checkInterval + ", checking for modification"); } //If the resource has not been modified return the cached resource. final boolean resourceModified = this.checkIfModified(cachedResource); if (!resourceModified) { cachedResource.setLastCheckTime(System.currentTimeMillis()); this.resourceCache.put( element); //do a cache put to notify the cache the object has been modified return cachedResource; } //The resource has been modified, reload it. cachedResource = this.loadResource(resource, builder); //Cache the loaded resource this.resourceCache.put(new Element(resource, cachedResource)); if (this.logger.isDebugEnabled()) { this.logger.debug("Loaded and cached " + cachedResource); } return cachedResource; } /** Check if any of the Resources used to load the {@link CachedResource} have been modified. */ protected <T> boolean checkIfModified(CachedResource<T> cachedResource) { final Resource resource = cachedResource.getResource(); //Check if the resource has been modified since it was last loaded. final long lastLoadTime = cachedResource.getLastLoadTime(); final long mainLastModified = this.getLastModified(resource); boolean resourceModified = lastLoadTime < mainLastModified; if (resourceModified) { this.logger.trace( "Resource {} was modified at {}, reloading", new Object[] {resource, mainLastModified}); return true; } //If the main resource hasn't changed check additional resources for modifications for (final Map.Entry<Resource, Long> additionalResourceEntry : cachedResource.getAdditionalResources().entrySet()) { final Resource additionalResource = additionalResourceEntry.getKey(); final Long resourceLastLoadTime = additionalResourceEntry.getValue(); final long lastModified = this.getLastModified(additionalResource); if (resourceLastLoadTime < lastModified) { this.logger.trace( "Additional resource {} for {} was modified at {}, reloading", new Object[] {additionalResource, resource, lastModified}); return true; } } this.logger.trace( "{} has not been modified since last loaded {}, returning", cachedResource, lastLoadTime); return false; } /** Determine the last modified time stamp for the resource */ protected long getLastModified(Resource resource) { try { return resource.lastModified(); } catch (IOException e) { this.logger.warn( "Could not determine lastModified for " + resource + ". This resource will never be reloaded due to lastModified changing.", e); } return 0; } private <T> CachedResource<T> loadResource(Resource resource, Loader<T> builder) throws IOException { final long lastLoadTime = System.currentTimeMillis(); long lastModified = 0; try { lastModified = resource.lastModified(); } catch (IOException e) { //Ignore, not all resources can have a valid lastModified returned } //Build the resource using the callback final LoadedResource<T> loadedResource = builder.loadResource(resource); final Serializable cacheKey = (Serializable) Arrays.asList(lastModified, loadedResource.getAdditionalResources()); //Create the CachedResource based on if digesting was enabled return new CachedResourceImpl<T>(resource, loadedResource, lastLoadTime, cacheKey); } private static class GetResourceArguments<T> { public final Resource resource; public final Loader<T> builder; public GetResourceArguments(Resource resource, Loader<T> builder) { this.resource = resource; this.builder = builder; } } private class CachedResourceEntryFactory extends ThreadLocalCacheEntryFactory<GetResourceArguments<?>> { @Override protected Object createEntry(Object key, GetResourceArguments<?> threadData) throws Exception { return loadResource(threadData.resource, threadData.builder); } } }