package com.temenos.interaction.loader.classloader;
/*
* #%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.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.MessageDigest;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.primitives.Longs;
import com.temenos.interaction.core.loader.FileEvent;
import com.temenos.interaction.loader.objectcreation.ParameterizedFactory;
/**
* This class manages the creation of URLClassLoaders, which are responsible for making new
* InteractionCommands available.
* It maintains a hashed state of the last jar file dynamically loaded based on the modified time.
* If an event arrives with information about new jars being available, the hash is calculated for the
* new jars and compared to the stored hash. If the hash values are equal, the cached URLClassLoader is
* simply returned.
* If the hash values are not equal a temporary directory is created and the jar files are copied to this directory.
* This circumvents the problem of the URLClassLoader holding the jar file open, making it impossible to replace it
* with a new version. A ParentLastURLClassloader is created to load the jars and the hash value is stored for future
* comparisons.
*
* @author trojanbug
*/
public class CachingParentLastURLClassloaderFactory implements ParameterizedFactory<FileEvent<File>, ClassLoader> {
private static final Logger LOGGER = LoggerFactory.getLogger(CachingParentLastURLClassloaderFactory.class);
private URLClassLoader cache = null;
private Object lastState = null;
private File lastClassloaderTempDir = null;
@Override
public synchronized ClassLoader getForObject(FileEvent<File> param) {
Object state = calculateCurrentState(param);
if (lastState == null || (!lastState.equals(state))) {
LOGGER.debug("Detected state change, creating new classloader");
Object previousState = lastState;
URLClassLoader previousCL = cache;
File previousTempDir = lastClassloaderTempDir;
cleanupClassloaderResources(previousCL, previousTempDir);
//TODO add listeners to inform about classloader creation and "destruction"
lastState = state;
cache = createClassLoader(state, param);
}
return cache;
}
protected synchronized URLClassLoader createClassLoader(Object currentState, FileEvent<File> param) {
try {
LOGGER.debug("Classloader requested from CachingParentLastURLClassloaderFactory, based on FileEvent reflecting change in {}", param.getResource().getAbsolutePath());
Set<URL> urls = new HashSet<URL>();
File newTempDir = new File(FileUtils.getTempDirectory(),currentState.toString());
FileUtils.forceMkdir(newTempDir);
Collection<File> files = FileUtils.listFiles(param.getResource(), new String[]{"jar"}, true);
for (File f : files) {
try {
LOGGER.trace("Adding {} to list of URLs to create classloader from", f.toURI().toURL());
FileUtils.copyFileToDirectory(f, newTempDir);
urls.add(new File(newTempDir, f.getName()).toURI().toURL());
} catch (MalformedURLException ex) {
// should not happen, we do have the file there
// but if, what can we do - just log it
LOGGER.warn("Trying to intilialize classloader based on URL failed!", ex);
}
}
lastClassloaderTempDir = newTempDir;
URLClassLoader classloader = new ParentLastURLClassloader(urls.toArray(new URL[]{}), Thread.currentThread().getContextClassLoader());
return classloader;
} catch (IOException ex) {
throw new RuntimeException("Unexpected error trying to create new classloader.", ex);
}
}
protected Object calculateCurrentState(FileEvent<File> param) {
Collection<File> files = FileUtils.listFiles(param.getResource(), new String[]{"jar"}, true);
MessageDigest md = DigestUtils.getMd5Digest();
for (File f : files) {
md.update(Longs.toByteArray(f.lastModified()));
}
Object state = Hex.encodeHexString(md.digest());
LOGGER.trace("Calculated representation /hash/ of state of collection of URLs for classloader creation to: {}", state);
return state;
}
private void cleanupClassloaderResources(URLClassLoader previousCL, File previousTempDir) {
try {
if(previousCL != null){
previousCL.close();
}
} catch (IOException ex) {
LOGGER.error("Failed to close classloader - potential resource and memory leak!", ex);
}
try {
if(previousTempDir != null){
FileUtils.forceDelete(previousTempDir);
}
} catch (IOException ex) {
LOGGER.error("Failed to delete temporary directory, possible resource leak!", ex);
}
}
}