package net.unicon.cas.addons.support; import java.net.URI; import org.apache.shiro.crypto.hash.Sha1Hash; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanCreationException; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.io.Resource; /** * A class responsible for detecting contents changes of configured resource (file, classpath, URL, etc.) * by comparing their SHA1 digests - the one saved at the last check and the latest one. If the change * is detected, it publishes Spring's <code>ApplicationEvent</code> typed as <code>ResourceChangedEvent</code> * wrapping the resource's URI in question within it. * <p/> * Any interested <code>ApplicationListener</code>s within ApplicationContext could then pick up those events and * react to them appropriately. * <p/> * <p>Note that the periodic polling of the configured resource is not a concern of this class and is configured outside of it. * Typically, within Spring ApplicationContext, it is done by <code>TaskScheduler</code> abstraction or the likes. * <p/> * <p>This class is thread-safe. It uses proper synchronization to protect <code>resourceSha1Hex</code> field updates from race conditions. * * @author Dmitriy Kopylenko * @author Unicon, inc. * @since 0.7 */ public class ResourceChangeDetectingEventNotifier implements ApplicationEventPublisherAware { /** * Application event representing the resource contents change. * Intended to be processed by subscribed <code>ApplicationListener</code>s * managed by ApplicationContext. */ public static class ResourceChangedEvent extends ApplicationEvent { private static final long serialVersionUID = 1L; private final URI resourceUri; public ResourceChangedEvent(final Object source, final URI resourceUri) { super(source); this.resourceUri = resourceUri; } public URI getResourceUri() { return this.resourceUri; } } private ApplicationEventPublisher applicationEventPublisher; private final Resource watchedResource; private volatile String resourceSha1Hex; private static final Logger logger = LoggerFactory.getLogger(ResourceChangeDetectingEventNotifier.class); private static final String EMPTY_STRING_SHA1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; public ResourceChangeDetectingEventNotifier(final Resource watchedResource) throws Exception { this.watchedResource = watchedResource; if (!this.watchedResource.exists()) { throw new BeanCreationException(String.format("The 'watchedResource' [%s] must point to an existing resource. " + "Please double-check that such resource exists.", this.watchedResource.getURI())); } //Initial SHA1 of the resource //TODO: currently assumes file-based resources. Think about refactoring later to support diff kinds of resources? final String initialSha1Hex = new Sha1Hash(this.watchedResource.getFile()).toHex(); if (EMPTY_STRING_SHA1.equals(initialSha1Hex)) { logger.warn("The 'watchedResource' [{}] is empty!", this.watchedResource.getURI()); } this.resourceSha1Hex = initialSha1Hex; } /** * Compare the SHA1 digests (since last check and the latest) of the configured resource, and if change is detected, * publish the <code>ResourceChangeEvent</code> to ApplicationContext. */ public void notifyOfTheResourceChangeEventIfNecessary() { final String currentResourceSha1 = this.resourceSha1Hex; String newResourceSha1 = null; try { newResourceSha1 = new Sha1Hash(this.watchedResource.getFile()).toHex(); if (!newResourceSha1.equals(currentResourceSha1)) { logger.debug("Resource: [{}] | Old Hash: [{}] | New Hash: [{}]", new Object[] {this.watchedResource.getURI(), currentResourceSha1, newResourceSha1}); synchronized (this.resourceSha1Hex) { this.resourceSha1Hex = newResourceSha1; this.applicationEventPublisher.publishEvent(new ResourceChangedEvent(this, this.watchedResource.getURI())); } } } catch (final Throwable e) { //TODO: Possibly introduce an exception handling strategy? logger.error("An exception is caught during 'watchedResource' access", e); return; } } @Override public void setApplicationEventPublisher(final ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } }