package net.unicon.cas.addons.serviceregistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.unicon.cas.addons.support.GuardedBy;
import net.unicon.cas.addons.support.ResourceChangeDetectingEventNotifier;
import net.unicon.cas.addons.support.ThreadSafe;
import org.jasig.cas.services.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Implementation of <code>ServiceRegistryDao</code> that reads services definition from JSON configuration file at the Spring Application Context
* initialization time. After un-marshaling services from JSON blob, delegates the storage and retrieval to <code>InMemoryServiceRegistryDaoImpl</code>
* <p/>
* This class implements ${link ApplicationListener<ResourceChangeDetectingEventNotifier.ResourceChangedEvent>} to reload services definitions in real-time.
* This class is thread safe.
*
* @author Dmitriy Kopylenko
* @author Unicon, inc.
* @since 0.8
*/
@ThreadSafe
public class JsonServiceRegistryDao implements ServiceRegistryDao,
ApplicationListener<ResourceChangeDetectingEventNotifier.ResourceChangedEvent> {
@GuardedBy("mutexMonitor")
private final InMemoryServiceRegistryDaoImpl delegateServiceRegistryDao = new InMemoryServiceRegistryDaoImpl();
protected final ObjectMapper objectMapper = new ObjectMapper();
protected final Resource servicesConfigFile;
private ReloadableServicesManager servicesManager;
private final Object mutexMonitor = new Object();
private static final String REGEX_PREFIX = "^";
protected static final String SERVICES_KEY = "services";
private static final String SERVICES_ID_KEY = "serviceId";
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
public JsonServiceRegistryDao(final Resource servicesConfigFile) {
this.servicesConfigFile = servicesConfigFile;
}
public final void setServicesManager(final ReloadableServicesManager servicesManager) {
this.servicesManager = servicesManager;
}
@Override
public final RegisteredService save(RegisteredService registeredService) {
synchronized (this.mutexMonitor) {
return saveInternal(registeredService);
}
}
@Override
public final boolean delete(RegisteredService registeredService) {
synchronized (this.mutexMonitor) {
return deleteInternal(registeredService);
}
}
@Override
public final RegisteredService findServiceById(long id) {
synchronized (this.mutexMonitor) {
return this.delegateServiceRegistryDao.findServiceById(id);
}
}
@Override
public final List<RegisteredService> load() {
synchronized (this.mutexMonitor) {
return this.delegateServiceRegistryDao.load();
}
}
protected RegisteredService saveInternal(final RegisteredService registeredService) {
return this.delegateServiceRegistryDao.save(registeredService);
}
protected boolean deleteInternal(final RegisteredService registeredService) {
return this.delegateServiceRegistryDao.delete(registeredService);
}
/**
* This method is used as a Spring bean loadServices-method
* as well as the reloading method when the change in the services definition resource is detected at runtime
*/
@SuppressWarnings("unchecked")
public final List<RegisteredService> loadServices() {
logger.info("Loading Registered Services from: [ {} ]...", this.servicesConfigFile);
final List<RegisteredService> resolvedServices = new ArrayList<RegisteredService>();
try {
final Map<String, List> m = unmarshalServicesRegistryResourceIntoMap();
if (m != null) {
final Iterator<Map> i = m.get(SERVICES_KEY).iterator();
while (i.hasNext()) {
final Map<?, ?> record = i.next();
final String svcId = ((String) record.get(SERVICES_ID_KEY));
final RegisteredService svc = getRegisteredServiceInstance(svcId);
if (svc != null) {
resolvedServices.add(this.objectMapper.convertValue(record, svc.getClass()));
logger.debug("Unmarshaled {}: {}", svc.getClass().getSimpleName(), record);
}
}
synchronized (this.mutexMonitor) {
this.delegateServiceRegistryDao.setRegisteredServices(resolvedServices);
}
}
} catch (final Throwable e) {
throw new RuntimeException(e);
}
return resolvedServices;
}
private Map<String, List> unmarshalServicesRegistryResourceIntoMap() throws IOException {
try {
final InputStream stream = this.servicesConfigFile.getInputStream();
if (stream != null) {
return this.objectMapper.readValue(stream, Map.class);
}
} catch (final FileNotFoundException e) {
logger.warn("Resource [{}] does not exist or has no service definitions.", this.servicesConfigFile);
}
return null;
}
private boolean isValidRegexPattern(final String pattern) {
boolean valid = false;
try {
if (pattern.startsWith(REGEX_PREFIX)) {
Pattern.compile(pattern);
valid = true;
}
} catch (final PatternSyntaxException e) {
logger.debug("Failed to identify [{}] as a regular expression", pattern);
}
return valid;
}
/**
* Constructs an instance of {@link RegisteredServiceWithAttributes} based on the
* syntax of the pattern defined. If the pattern is considered a valid regular expression,
* an instance of {@link RegexRegisteredServiceWithAttributes} is created. Otherwise,
* {@link RegisteredServiceWithAttributesImpl}.
* @see #isValidRegexPattern(String)
* @param pattern the pattern of the service definition
* @return an instance of {@link RegisteredServiceWithAttributes}
*/
private RegisteredService getRegisteredServiceInstance(final String pattern) {
if (isValidRegexPattern(pattern)) {
return new RegexRegisteredServiceWithAttributes();
}
return new RegisteredServiceWithAttributesImpl();
}
@Override
public final void onApplicationEvent(final ResourceChangeDetectingEventNotifier.ResourceChangedEvent resourceChangedEvent) {
try {
if (!resourceChangedEvent.getResourceUri().equals(this.servicesConfigFile.getURI())) {
//Not our resource. Just get out of here.
return;
}
}
catch (final Throwable e) {
logger.error("An exception is caught while trying to access JSON resource: ", e);
return;
}
logger.debug("Received change event for JSON resource {}. Reloading services...", resourceChangedEvent.getResourceUri());
synchronized (this.mutexMonitor) {
loadServices();
this.servicesManager.reload();
}
}
/**
* Spring infrastructure class to support circular references DI of ReloadableServicesManager and JsonServiceRegistryDao
* required to make real-time reloading behavior work with disabling the default CAS periodic polling
* reloading behavior which avoids unnecessary additional reloading.
*/
@Component
public static class ServicesManagerInjectableBeanPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
ReloadableServicesManager servicesManager;
JsonServiceRegistryDao serviceRegistryDao;
try {
servicesManager = beanFactory.getBean(ReloadableServicesManager.class);
serviceRegistryDao = beanFactory.getBean(JsonServiceRegistryDao.class);
}
catch (NoSuchBeanDefinitionException e) {
return;
}
serviceRegistryDao.setServicesManager(servicesManager);
}
}
}