package org.apereo.cas.services; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.util.ResourceUtils; import org.apereo.cas.util.io.LockedOutputStream; import org.apereo.cas.util.serialization.StringSerializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.io.Resource; import org.springframework.util.Assert; import javax.annotation.PreDestroy; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.Watchable; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; /** * This is {@link AbstractResourceBasedServiceRegistryDao}. * * @author Misagh Moayyed * @since 5.0.0 */ public abstract class AbstractResourceBasedServiceRegistryDao implements ResourceBasedServiceRegistryDao { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractResourceBasedServiceRegistryDao.class); /** * The Service registry directory. */ protected Path serviceRegistryDirectory; /** * Map of service ID to registered service. */ private Map<Long, RegisteredService> serviceMap = new ConcurrentHashMap<>(); /** * The Registered service json serializer. */ private StringSerializer<RegisteredService> registeredServiceSerializer; private Thread serviceRegistryWatcherThread; private ServiceRegistryConfigWatcher serviceRegistryConfigWatcher; /** * Instantiates a new service registry dao. * * @param configDirectory the config directory * @param serializer the registered service json serializer * @param enableWatcher enable watcher thread * @param eventPublisher the event publisher */ public AbstractResourceBasedServiceRegistryDao(final Path configDirectory, final StringSerializer<RegisteredService> serializer, final boolean enableWatcher, final ApplicationEventPublisher eventPublisher) { initializeRegistry(configDirectory, serializer, enableWatcher, eventPublisher); } /** * Instantiates a new Abstract resource based service registry dao. * * @param configDirectory the config directory * @param serializer the serializer * @param enableWatcher the enable watcher * @param eventPublisher the event publisher * @throws Exception the exception */ public AbstractResourceBasedServiceRegistryDao(final Resource configDirectory, final StringSerializer<RegisteredService> serializer, final boolean enableWatcher, final ApplicationEventPublisher eventPublisher) throws Exception { final Resource servicesDirectory = ResourceUtils.prepareClasspathResourceIfNeeded(configDirectory, true, getExtension()); initializeRegistry(Paths.get(servicesDirectory.getFile().getCanonicalPath()), serializer, enableWatcher, eventPublisher); } private void initializeRegistry(final Path configDirectory, final StringSerializer<RegisteredService> registeredServiceJsonSerializer, final boolean enableWatcher, final ApplicationEventPublisher eventPublisher) { this.serviceRegistryDirectory = configDirectory; Assert.isTrue(this.serviceRegistryDirectory.toFile().exists(), this.serviceRegistryDirectory + " does not exist"); Assert.isTrue(this.serviceRegistryDirectory.toFile().isDirectory(), this.serviceRegistryDirectory + " is not a directory"); this.registeredServiceSerializer = registeredServiceJsonSerializer; if (enableWatcher) { LOGGER.info("Watching service registry directory at [{}]", configDirectory); this.serviceRegistryConfigWatcher = new ServiceRegistryConfigWatcher(this, eventPublisher); this.serviceRegistryWatcherThread = new Thread(this.serviceRegistryConfigWatcher); this.serviceRegistryWatcherThread.setName(this.getClass().getName()); this.serviceRegistryWatcherThread.start(); LOGGER.debug("Started service registry watcher thread"); } } /** * Destroy the watch service thread. */ @PreDestroy public void destroy() { if (this.serviceRegistryConfigWatcher != null) { this.serviceRegistryConfigWatcher.close(); } if (serviceRegistryWatcherThread != null) { this.serviceRegistryWatcherThread.interrupt(); } } @Override public long size() { return this.serviceMap.size(); } @Override public RegisteredService findServiceById(final long id) { return this.serviceMap.get(id); } @Override public RegisteredService findServiceById(final String id) { return this.serviceMap.values() .stream() .filter(r -> r.matches(id)) .findFirst() .orElse(null); } @Override public String toString() { return getClass().getSimpleName(); } @Override public synchronized boolean delete(final RegisteredService service) { try { final File f = makeFile(service); final boolean result = f.delete(); if (!result) { LOGGER.warn("Failed to delete service definition file [{}]", f.getCanonicalPath()); } else { this.serviceMap.remove(service.getId()); LOGGER.debug("Successfully deleted service definition file [{}]", f.getCanonicalPath()); } return result; } catch (final IOException e) { throw new IllegalArgumentException(e); } } @Override public synchronized List<RegisteredService> load() { final Map<Long, RegisteredService> temp = new ConcurrentHashMap<>(); final Collection<File> c = FileUtils.listFiles(this.serviceRegistryDirectory.toFile(), new String[]{getExtension()}, true); c.stream().filter(file -> file.length() > 0).forEach(file -> { final RegisteredService service = load(file); if (service == null) { LOGGER.error("Could not load service definition from file [{}]", file); } else { if (temp.containsKey(service.getId())) { LOGGER.warn("Found a service definition [{}] with a duplicate id [{}]. " + "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()); } temp.put(service.getId(), service); } }); this.serviceMap = temp.entrySet() .stream() .sorted(Map.Entry.comparingByValue()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); return new ArrayList(this.serviceMap.values()); } /** * Load registered service from file. * * @param file the file * @return the registered service, or null if file cannot be read, is not found, is empty or parsing error occurs. */ @Override public RegisteredService load(final File file) { if (!file.canRead()) { LOGGER.warn("[{}] is not readable. Check file permissions", file.getName()); return null; } if (!file.exists()) { LOGGER.warn("[{}] is not found at the path specified", file.getName()); return null; } if (file.length() == 0) { LOGGER.debug("[{}] appears to be empty so no service definition will be loaded", file.getName()); return null; } try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) { return this.registeredServiceSerializer.from(in); } catch (final Exception e) { LOGGER.error("Error reading configuration file [{}]", file.getName(), e); } return null; } @Override public RegisteredService save(final RegisteredService service) { if (service.getId() == RegisteredService.INITIAL_IDENTIFIER_VALUE && service instanceof AbstractRegisteredService) { LOGGER.debug("Service id not set. Calculating id based on system time..."); ((AbstractRegisteredService) service).setId(System.currentTimeMillis()); } final File f = makeFile(service); try (LockedOutputStream out = new LockedOutputStream(new FileOutputStream(f))) { this.registeredServiceSerializer.to(out, service); if (this.serviceMap.containsKey(service.getId())) { LOGGER.debug("Found existing service definition by id [{}]. Saving...", service.getId()); } this.serviceMap.put(service.getId(), service); LOGGER.debug("Saved service to [{}]", f.getCanonicalPath()); } catch (final IOException e) { throw new IllegalArgumentException("IO error opening file stream.", e); } return findServiceById(service.getId()); } /** * Creates a file for a registered service. * The file is named as {@code [SERVICE-NAME]-[SERVICE-ID]-.{@value #getExtension()}} * * @param service Registered service. * @return file in service registry directory. * @throws IllegalArgumentException if file name is invalid */ protected File makeFile(final RegisteredService service) { final String fileName = StringUtils.remove(service.getName() + '-' + service.getId() + '.' + getExtension(), " "); try { final File svcFile = new File(this.serviceRegistryDirectory.toFile(), fileName); LOGGER.debug("Using [{}] as the service definition file", svcFile.getCanonicalPath()); return svcFile; } catch (final IOException e) { LOGGER.warn("Service file name [{}] is invalid; Examine for illegal characters in the name.", fileName); throw new IllegalArgumentException(e); } } /** * Gets extension associated with files in the given resource directory. * * @return the extension */ protected abstract String getExtension(); @Override public <T extends Watchable> T getWatchableResource() { final Watchable watchable = this.serviceRegistryDirectory; return (T) watchable; } @Override public void update(final RegisteredService service) { this.serviceMap.put(service.getId(), service); } }