package com.temenos.interaction.loader.detector;
/*
* #%L
* interaction-dynamic-loader
* %%
* Copyright (C) 2012 - 2015 Temenos Holdings N.V.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import java.io.Closeable;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import org.reflections.Reflections;
import org.reflections.scanners.AbstractScanner;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.vfs.Vfs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.temenos.interaction.core.command.ChainingCommandController;
import com.temenos.interaction.core.command.CommandController;
import com.temenos.interaction.core.command.InteractionCommand;
import com.temenos.interaction.core.command.SpringContextBasedInteractionCommandController;
import com.temenos.interaction.core.loader.Action;
import com.temenos.interaction.core.loader.FileEvent;
import com.temenos.interaction.loader.classloader.CachingParentLastURLClassloaderFactory;
import com.temenos.interaction.loader.objectcreation.ParameterizedFactory;
/**
* Loads a CommandController with a set of InteractionCommands from Spring
* configuration files.
*
* The execute method would be typically called after a directory change (for
* instance, when a user copies a jar file with new InteractionCommands). All
* jars in the directory are scanned for Spring configuration files matching the
* pattern "/spring/*-interaction-context.xml". By default, the
* CommandController with id "commandController", together with the defined
* InteractionCommand, would be loaded to the top of a provided
* ChainingCommandController.
*
* @author andres
* @author trojan
* @author cmclopes
*/
public class SpringBasedLoaderAction implements Action<FileEvent<File>>, ApplicationContextAware, InitializingBean {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringBasedLoaderAction.class);
public static final String DEFAULT_COMMAND_CONTROLLER_BEAN_NAME = "commandController";
List<String> configPatterns = new ArrayList();
private ApplicationContext currentContext = null;
private ApplicationContext parentContext = null;
private boolean useCurrentContextAsParent = false;
private List<String> configLocationsPatterns = new ArrayList(Arrays.asList(new String[]{"classpath:/spring*/*-interaction-context.xml"}));
private Collection<? extends Action<ApplicationContext>> listeners = new ArrayList();
ParameterizedFactory<FileEvent<File>, ClassLoader> classloaderFactory = new CachingParentLastURLClassloaderFactory();
private String commandControllerBeanName = DEFAULT_COMMAND_CONTROLLER_BEAN_NAME;
private ChainingCommandController parentChainingCommandController = null;
private CommandController previouslyAddedCommandController = null;
private ApplicationContext previousAppCtx = null;
@Override
public void execute(FileEvent<File> dirEvent) {
LOGGER.debug("Creation of new Spring ApplicationContext based CommandController triggerred by change in", dirEvent.getResource().getAbsolutePath());
Collection<File> jars = FileUtils.listFiles(dirEvent.getResource(), new String[]{"jar"}, true);
Set<URL> urls = new HashSet();
for (File f : jars) {
try {
LOGGER.trace("Adding {} to list of URLs to create ApplicationContext from", f.toURI().toURL());
urls.add(f.toURI().toURL());
} catch (MalformedURLException ex) {
// kindly ignore and log
}
}
Reflections reflectionHelper = new Reflections(
new ConfigurationBuilder()
.addClassLoader(classloaderFactory.getForObject(dirEvent)).addScanners(new ResourcesScanner())
.addUrls(urls)
);
Set<String> resources = new HashSet();
for (String locationPattern : configLocationsPatterns) {
String regex = convertWildcardToRegex(locationPattern);
resources.addAll(reflectionHelper.getResources(Pattern.compile(regex)));
}
if (!resources.isEmpty()) {
// if resources are empty just clean up the previous ApplicationContext and leave!
LOGGER.debug("Detected potential Spring config files to load");
ClassPathXmlApplicationContext context;
if (parentContext != null) {
context = new ClassPathXmlApplicationContext(parentContext);
} else {
context = new ClassPathXmlApplicationContext();
}
context.setConfigLocations(configLocationsPatterns.toArray(new String[]{}));
ClassLoader childClassLoader = classloaderFactory.getForObject(dirEvent);
context.setClassLoader(childClassLoader);
context.refresh();
CommandController cc = null;
try {
cc = context.getBean(commandControllerBeanName, CommandController.class);
LOGGER.debug("Detected pre-configured CommandController in added config files");
} catch (BeansException ex) {
if(LOGGER.isDebugEnabled()) {
LOGGER.debug("No detected pre-configured CommandController in added config files.", ex);
}
Map<String, InteractionCommand> commands = context.getBeansOfType(InteractionCommand.class);
if (!commands.isEmpty()) {
LOGGER.debug("Adding new commands");
SpringContextBasedInteractionCommandController scbcc = new SpringContextBasedInteractionCommandController();
scbcc.setApplicationContext(context);
cc = scbcc;
} else {
LOGGER.debug("No commands detected to be added");
}
}
if (parentChainingCommandController != null) {
List<CommandController> newCommandControllers = new ArrayList<CommandController>(parentChainingCommandController.getCommandControllers());
// "unload" the previously loaded CommandController
if (previouslyAddedCommandController != null) {
LOGGER.debug("Removing previously added instance of CommandController");
newCommandControllers.remove(previouslyAddedCommandController);
}
// if there is a new CommandController on the Spring file, add it on top of the chain
if (cc != null) {
LOGGER.debug("Adding newly created CommandController to ChainingCommandController");
newCommandControllers.add(0, cc);
parentChainingCommandController.setCommandControllers(newCommandControllers);
previouslyAddedCommandController = cc;
} else {
previouslyAddedCommandController = null;
}
} else {
LOGGER.debug("No ChainingCommandController set to add newly created CommandController to - skipping action");
}
if (previousAppCtx != null) {
if (previousAppCtx instanceof Closeable) {
try {
((Closeable) previousAppCtx).close();
} catch (Exception ex) {
LOGGER.error("Error closing the ApplicationContext.", ex);
}
}
previousAppCtx = context;
}
} else {
LOGGER.debug("No Spring config files detected in the JARs scanned");
}
}
@Override
public void setApplicationContext(ApplicationContext ac) throws BeansException {
currentContext = ac;
}
@Override
public void afterPropertiesSet() throws Exception {
if (parentContext == null && currentContext != null && useCurrentContextAsParent) {
parentContext = currentContext;
}
}
/**
* @return the listeners
*/
public Collection<? extends Action<ApplicationContext>> getListeners() {
return listeners;
}
/**
* @param listeners the listeners to set
*/
public void setListeners(Collection<? extends Action<ApplicationContext>> listeners) {
this.listeners = new ArrayList(listeners);
}
/**
* @return the classloaderFactory
*/
public ParameterizedFactory<FileEvent<File>, ClassLoader> getClassloaderFactory() {
return classloaderFactory;
}
/**
* @param classloaderFactory the classloaderFactory to set
*/
public void setClassloaderFactory(ParameterizedFactory<FileEvent<File>, ClassLoader> classloaderFactory) {
this.classloaderFactory = classloaderFactory;
}
/**
* @return the parentContext
*/
public ApplicationContext getParentContext() {
return parentContext;
}
/**
* @param parentContext the parentContext to set
*/
public void setParentContext(ApplicationContext parentContext) {
this.parentContext = parentContext;
}
/**
* @return the useCurrentContextAsParent
*/
public boolean isUseCurrentContextAsParent() {
return useCurrentContextAsParent;
}
/**
* @param useCurrentContextAsParent the useCurrentContextAsParent to set
*/
public void setUseCurrentContextAsParent(boolean useCurrentContextAsParent) {
this.useCurrentContextAsParent = useCurrentContextAsParent;
}
/**
* @return the configLocationsPatterns
*/
public List<String> getConfigLocationsPatterns() {
return configLocationsPatterns;
}
/**
* @param configLocationsPatterns the configLocationsPatterns to set
*/
public void setConfigLocationsPatterns(List<String> configLocationsPatterns) {
this.configLocationsPatterns = configLocationsPatterns;
}
/**
* @return the commandControllerBeanName
*/
public String getCommandControllerBeanName() {
return commandControllerBeanName;
}
/**
* @param commandControllerBeanName the commandControllerBeanName to set
*/
public void setCommandControllerBeanName(String commandControllerBeanName) {
this.commandControllerBeanName = commandControllerBeanName;
}
/**
* @return the parentChainingCommandController
*/
public ChainingCommandController getParentChainingCommandController() {
return parentChainingCommandController;
}
/**
* @param parentChainingCommandController the
* parentChainingCommandController to set
*/
public void setParentChainingCommandController(ChainingCommandController parentChainingCommandController) {
this.parentChainingCommandController = parentChainingCommandController;
}
public static class CurrentThreadClassLoaderFactory implements ParameterizedFactory<FileEvent<File>, ClassLoader> {
public CurrentThreadClassLoaderFactory() {
}
@Override
public ClassLoader getForObject(FileEvent<File> param) {
return Thread.currentThread().getContextClassLoader();
}
}
private String convertWildcardToRegex(String wildcardPattern) {
wildcardPattern = wildcardPattern.substring(wildcardPattern.indexOf(':') + 1);
wildcardPattern = wildcardPattern.substring(wildcardPattern.lastIndexOf("/") + 1);
return wildcardPattern.replaceAll("\\*", "[^/]*");
}
public static class ResourcesScanner extends AbstractScanner {
public boolean acceptsInput(String file) {
return !file.endsWith(".class"); //not a class
}
@Override
public Object scan(Vfs.File file, Object classObject) {
getStore().put(file.getName(), file.getRelativePath());
return classObject;
}
public void scan(Object cls) {
throw new UnsupportedOperationException(); //shouldn't get here
}
}
}