package org.apereo.cas.services;
import com.google.common.base.Throwables;
import org.apache.commons.io.IOUtils;
import org.apereo.cas.support.events.service.CasRegisteredServicesRefreshEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import static java.nio.file.StandardWatchEventKinds.*;
/**
* This is {@link ServiceRegistryConfigWatcher} that watches the json config directory
* for changes and promptly attempts to reload the CAS service registry configuration.
*
* @author Misagh Moayyed
* @since 4.1.0
*/
class ServiceRegistryConfigWatcher implements Runnable, Closeable {
private static final Logger LOGGER = LoggerFactory.getLogger(ServiceRegistryConfigWatcher.class);
private final AtomicBoolean running = new AtomicBoolean(false);
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = this.lock.readLock();
private final WatchService watcher;
private final ResourceBasedServiceRegistryDao serviceRegistryDao;
private ApplicationEventPublisher applicationEventPublisher;
/**
* Instantiates a new Json service registry config watcher.
*
* @param serviceRegistryDao the registry to callback
*/
ServiceRegistryConfigWatcher(final ResourceBasedServiceRegistryDao serviceRegistryDao,
final ApplicationEventPublisher eventPublisher) {
try {
this.serviceRegistryDao = serviceRegistryDao;
this.watcher = FileSystems.getDefault().newWatchService();
final WatchEvent.Kind[] kinds = new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
LOGGER.debug("Created service registry watcher for events of type [{}]", (Object[]) kinds);
this.serviceRegistryDao.getWatchableResource().register(this.watcher, kinds);
this.applicationEventPublisher = eventPublisher;
} catch (final IOException e) {
throw Throwables.propagate(e);
}
}
@Override
public void run() {
if (this.running.compareAndSet(false, true)) {
while (this.running.get()) {
// wait for key to be signaled
WatchKey key = null;
try {
key = this.watcher.take();
handleEvent(key);
} catch (final InterruptedException e) {
return;
} finally {
/*
Reset the key -- this step is critical to receive
further watch events. If the key is no longer valid, the directory
is inaccessible so exit the loop.
*/
final boolean valid = key != null && key.reset();
if (!valid) {
LOGGER.warn("Directory key is no longer valid. Quitting watcher service");
}
}
}
}
}
/**
* Handle event.
*
* @param key the key
*/
private void handleEvent(final WatchKey key) {
this.readLock.lock();
try {
//The filename is the context of the event.
key.pollEvents().stream().filter(event -> event.count() <= 1).forEach(event -> {
final WatchEvent.Kind kind = event.kind();
//The filename is the context of the event.
final WatchEvent<Path> ev = (WatchEvent<Path>) event;
final Path filename = ev.context();
final Path parent = (Path) key.watchable();
final Path fullPath = parent.resolve(filename);
final File file = fullPath.toFile();
LOGGER.trace("Detected event [{}] on file [{}]. Loading change...", kind, file);
if (kind.name().equals(ENTRY_CREATE.name()) && file.exists()) {
handleCreateEvent(file);
} else if (kind.name().equals(ENTRY_DELETE.name())) {
handleDeleteEvent();
} else if (kind.name().equals(ENTRY_MODIFY.name()) && file.exists()) {
handleModifyEvent(file);
}
});
} finally {
this.readLock.unlock();
}
}
/**
* Handle modify event.
*
* @param file the file
*/
private void handleModifyEvent(final File file) {
final RegisteredService newService = this.serviceRegistryDao.load(file);
if (newService == null) {
LOGGER.warn("New service definition could not be loaded from [{}]", file.getAbsolutePath());
} else {
final RegisteredService oldService = this.serviceRegistryDao.findServiceById(newService.getId());
if (!newService.equals(oldService)) {
this.serviceRegistryDao.update(newService);
this.applicationEventPublisher.publishEvent(new CasRegisteredServicesRefreshEvent(this));
} else {
LOGGER.debug("Service [{}] loaded from [{}] is identical to the existing entry. Entry may have already been saved "
+ "in the event processing pipeline", newService.getId(), file.getName());
}
}
}
/**
* Handle delete event.
*/
private void handleDeleteEvent() {
this.serviceRegistryDao.load();
this.applicationEventPublisher.publishEvent(new CasRegisteredServicesRefreshEvent(this));
}
/**
* Handle create event.
*
* @param file the file
*/
private void handleCreateEvent(final File file) {
//load the entry and add it to the map
final RegisteredService service = this.serviceRegistryDao.load(file);
if (service == null) {
LOGGER.warn("No service definition was loaded from [{}]", file);
return;
}
if (this.serviceRegistryDao.findServiceById(service.getId()) != null) {
LOGGER.warn("Found a service definition [{}] with a duplicate id [{}] in [{}]. "
+ "This will overwrite previous service definitions and is likely a "
+ "configuration problem. Make sure all services have a unique id and try again.",
service.getServiceId(), service.getId(), file.getAbsolutePath());
}
this.serviceRegistryDao.update(service);
this.applicationEventPublisher.publishEvent(new CasRegisteredServicesRefreshEvent(this));
}
@Override
public void close() {
IOUtils.closeQuietly(this.watcher);
}
public void setApplicationEventPublisher(final ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
}