package org.xbib.elasticsearch.action.deploy;
import org.elasticsearch.ElasticSearchException;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.xbib.classloader.uri.URIClassLoader;
import org.xbib.elasticsearch.gatherer.Gatherer;
import org.xbib.elasticsearch.gatherer.GathererService;
import org.xbib.io.StreamUtil;
import org.xbib.io.archivers.zip.ZipArchiveEntry;
import org.xbib.io.archivers.zip.ZipFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.Enumeration;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import static org.elasticsearch.common.collect.Maps.newHashMap;
import static org.elasticsearch.common.collect.Sets.newHashSet;
/**
* The deploy service is a service for deploying gatherer plugin jars to a node.
*
* It must be initialized as a singleton.
*
* Gatherers are added to this service by the gatherer name and a valid path name.
* The path name is included into a separate URI classloader and searched for
* es-gatherer.properties for instantiating Gatherer instances.
*
* All gatherer instances are collected in a map with the gatherer name as key.
*
*/
public class DeployService extends AbstractLifecycleComponent<DeployService> {
private final URIClassLoader deployClassLoader;
private final GathererService gathererService;
@Inject
public DeployService(Settings settings, GathererService gathererService) {
super(settings);
this.deployClassLoader = new URIClassLoader(settings.getClass().getClassLoader());
this.gathererService = gathererService;
}
@Override
protected void doStart() throws ElasticSearchException {
}
@Override
protected void doStop() throws ElasticSearchException {
}
@Override
protected void doClose() throws ElasticSearchException {
}
/**
* Add JAR to gatherer registry
*
* @param name the prefix to register the gatherer package under
* @param path the path of JAR
*/
public void add(String name, String path) throws IOException {
// new class loader for each gatherer
URIClassLoader uriClassLoader = new URIClassLoader(deployClassLoader);
// try to unpack (zip)
tryUnpackArchive(path);
// find all jars
Set<URI> jars = newHashSet();
findJars(path, jars);
for (URI jar : jars) {
uriClassLoader.addURI(jar);
}
gathererService.getRegistry().getGatherers()
.putAll(loadGathererFromClasspath(name, uriClassLoader));
logger.info("installed {}", path);
for (URI uri : uriClassLoader.getURIs()) {
logger.info("class path member {}", uri);
}
logger.info("registry = {}", gathererService.getRegistry());
gathererService.announceGatherers();
}
private Map<String, Gatherer> loadGathererFromClasspath(String prefix, ClassLoader classLoader) {
Map<String, Gatherer> gatherers = newHashMap();
Enumeration<URL> gathererUrls;
try {
gathererUrls = classLoader.getResources("es-gatherer.properties");
} catch (IOException e) {
logger.warn("failed to find gatherers in classpath", e);
return gatherers;
}
while (gathererUrls.hasMoreElements()) {
URL gathererUrl = gathererUrls.nextElement();
Properties gathererProps = new Properties();
InputStream is = null;
try {
is = gathererUrl.openStream();
gathererProps.load(is);
Gatherer gatherer = loadGatherer(gathererProps.getProperty("gatherer"), classLoader);
gatherers.put(prefix + "/" + gatherer.name(), gatherer);
} catch (Exception e) {
logger.warn("failed to load gatherer from [" + gathererUrl + "]", e);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// ignore
}
}
}
}
return gatherers;
}
@SuppressWarnings("unchecked")
private Gatherer loadGatherer(String className, ClassLoader classLoader) {
try {
Class<? extends Gatherer> gathererClass = (Class<? extends Gatherer>) classLoader.loadClass(className);
try {
return gathererClass.getConstructor(Settings.class).newInstance(settings);
} catch (NoSuchMethodException e) {
try {
return gathererClass.getConstructor().newInstance();
} catch (NoSuchMethodException e1) {
throw new ElasticSearchException("No constructor for [" + gathererClass + "]. A gatherer class must " +
"have either an empty default constructor or a single argument constructor accepting a " +
"Settings instance");
}
}
} catch (Exception e) {
throw new ElasticSearchException("Failed to load gatherer class [" + className + "]", e);
}
}
private void tryUnpackArchive(String path) throws IOException {
if (path ==null) {
return;
}
File file = new File(path);
if (!file.exists()) {
throw new IOException("file does not exist: " + file.getAbsolutePath());
}
if (!file.canRead()) {
throw new IOException("can not read: " + file.getAbsolutePath());
}
if (!file.getName().toLowerCase().endsWith(".zip")) {
return;
}
ZipFile zipFile = null;
try {
zipFile = new ZipFile(file);
//we check whether we need to remove the top-level folder while extracting
//sometimes (e.g. github) the downloaded archive contains a top-level folder which needs to be removed
boolean removeTopLevelDir = topLevelDirInExcess(zipFile);
Enumeration<? extends ZipArchiveEntry> zipEntries = zipFile.getEntries();
while (zipEntries.hasMoreElements()) {
ZipArchiveEntry zipEntry = zipEntries.nextElement();
if (zipEntry.isDirectory()) {
continue;
}
String zipEntryName = zipEntry.getName().replace('\\', '/');
if (removeTopLevelDir) {
zipEntryName = zipEntryName.substring(zipEntryName.indexOf('/'));
}
File target = new File(file.getParent(), zipEntryName);
StreamUtil.copy(zipFile.getInputStream(zipEntry), new FileOutputStream(target));
}
} catch (Exception e) {
logger.error("failed to extract " + file.getAbsolutePath(), e);
} finally {
if (zipFile != null) {
try {
zipFile.close();
} catch (IOException e) {
// ignore
}
}
// remove zip file
file.delete();
}
}
private boolean topLevelDirInExcess(ZipFile zipFile) {
//We don't rely on ZipEntry#isDirectory because it might be that there is no explicit dir
//but the files path do contain dirs, thus they are going to be extracted on sub-folders anyway
Enumeration<? extends ZipArchiveEntry> zipEntries = zipFile.getEntries();
Set<String> topLevelDirNames = newHashSet();
while (zipEntries.hasMoreElements()) {
ZipArchiveEntry zipEntry = zipEntries.nextElement();
String zipEntryName = zipEntry.getName().replace('\\', '/');
int slash = zipEntryName.indexOf('/');
//if there isn't a slash in the entry name it means that we have a file in the top-level
if (slash == -1) {
return false;
}
topLevelDirNames.add(zipEntryName.substring(0, slash));
//if we have more than one top-level folder
if (topLevelDirNames.size() > 1) {
return false;
}
}
return topLevelDirNames.size() == 1;
}
private void findJars(String path, Set<URI> jars) {
File root = new File( path );
File[] list = root.listFiles();
if (list == null) {
return;
}
for (File f : list) {
if (f.isDirectory()) {
findJars(f.getAbsolutePath(), jars);
} else {
if (f.getName().endsWith(".jar")) {
jars.add(URI.create("file:" + f.getAbsolutePath()));
}
}
}
}
}