/* Copyright (c) 2013-2014 Boundless and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/edl-v10.html
*
* Contributors:
* Victor Olaya (Boundless) - initial implementation
*/
package org.locationtech.geogig.api.hooks;
import static com.google.common.base.Preconditions.checkArgument;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.locationtech.geogig.api.AbstractGeoGigOp;
import org.locationtech.geogig.api.plumbing.ResolveRepository;
import org.locationtech.geogig.repository.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
/**
* Utilities to execute scripts representing hooks for GeoGig operations
*
*/
public class Scripting {
private static final Logger LOGGER = LoggerFactory.getLogger(Scripting.class);
private static final String PARAMS = "params";
private static final String GEOGIG = "geogig";
private static ScriptEngineManager factory = new ScriptEngineManager();
/**
* Runs a script
*
* @param scriptFile the script file to run
* @param operation the operation triggering the script, to provide context for the script. This
* object might get modified if the script modifies it to alter how the command is called
* (for instance, changing the commit message in a commit operation)
* @throws CannotRunGeogigOperationException
*/
@SuppressWarnings("unchecked")
public static void runJVMScript(AbstractGeoGigOp<?> operation, File scriptFile)
throws CannotRunGeogigOperationException {
checkArgument(scriptFile.exists(), "Script file does not exist %s", scriptFile.getPath());
LOGGER.info("Running jvm script {}", scriptFile.getAbsolutePath());
final String filename = scriptFile.getName();
final String ext = Files.getFileExtension(filename);
final ScriptEngine engine = factory.getEngineByExtension(ext);
try {
Map<String, Object> params = getParamMap(operation);
engine.put(PARAMS, params);
Repository repo = operation.command(ResolveRepository.class).call();
GeoGigAPI api = new GeoGigAPI(repo);
engine.put(GEOGIG, api);
engine.eval(new FileReader(scriptFile));
Object map = engine.get(PARAMS);
setParamMap((Map<String, Object>) map, operation);
} catch (ScriptException e) {
Throwable cause = Throwables.getRootCause(e);
// TODO: improve this hack to check exception type
if (cause != e) {
String msg = cause.getMessage();
msg = msg.substring(CannotRunGeogigOperationException.class.getName().length() + 2,
msg.lastIndexOf("(")).trim();
msg += " (command aborted by .geogig/hooks/" + scriptFile.getName() + ")";
throw new CannotRunGeogigOperationException(msg);
} else {
throw new CannotRunGeogigOperationException(String.format(
"Script %s threw an exception: '%s'", scriptFile, e.getMessage()), e);
}
} catch (Exception e) {
}
}
public static void runShellScript(final File scriptFile)
throws CannotRunGeogigOperationException {
LOGGER.info("Running shell script {}", scriptFile.getAbsolutePath());
// try running the script directly as an executable file
List<String> commandAndArgs = Lists.newArrayList();
ProcessBuilder pb = new ProcessBuilder(commandAndArgs);
if (isWindows()) {
commandAndArgs.add("cmd.exe");
commandAndArgs.add("/C");
commandAndArgs.add(scriptFile.getPath());
} else {
if (!scriptFile.canExecute()) {
return;
}
commandAndArgs.add(scriptFile.getPath());
}
try {
LOGGER.debug("-- starting process {}", scriptFile);
pb.redirectErrorStream(true);
final Process process = pb.start();
LOGGER.debug("-- process {} started", scriptFile);
final StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(),
System.out);
outputGobbler.start();
int exitCode;
try {
LOGGER.debug("-- waiting for process {} to finish", scriptFile);
exitCode = process.waitFor();
LOGGER.debug("process {} exit code: {}", scriptFile, exitCode);
} finally {
outputGobbler.stop();
}
if (exitCode != 0) {
// the script exited with non-zero code, so we indicate it throwing the
// corresponding exception.
// TODO: get message?
throw new CannotRunGeogigOperationException(
"Hook script exited with non-zero error code");
}
} catch (IOException e) {
e.printStackTrace();
return; // can't run scripts, so there is nothing that blocks running the
// command, and we can return
} catch (InterruptedException e) {
e.printStackTrace();
return; // can't run scripts, so there is nothing that blocks running the
// command, and we can return
}
}
/**
* Method for getting values of parameters, including private fields. This is to be used from
* scripting languages to create hooks for available commands. TODO: Review this and maybe
* change this way of accessing values
*
* @param operation
*
* @param param the name of the parameter
* @return the value of the parameter
*/
public static Map<String, Object> getParamMap(AbstractGeoGigOp<?> operation) {
Map<String, Object> map = Maps.newHashMap();
try {
Field[] fields = operation.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String name = field.getName();
Object value = field.get(operation);
map.put(name, value);
}
} catch (SecurityException e) {
return map;
} catch (IllegalArgumentException e) {
return map;
} catch (IllegalAccessException e) {
return map;
}
return map;
}
/**
* Method to set fields in the operation object. This is to be used to communicate with script
* hooks, so the operation can be modified in the hook, changing the values of its fields.
* Entries corresponding to inexistent fields are ignored
*
* @param operation
*
* @param a map of new field values. Keys are field names
*/
public static void setParamMap(Map<String, Object> map, AbstractGeoGigOp<?> operation) {
try {
Field[] fields = operation.getClass().getDeclaredFields();
Set<String> keys = map.keySet();
for (Field field : fields) {
final int modifiers = field.getModifiers();
if (field.isSynthetic() || Modifier.isStatic(modifiers)
|| Modifier.isFinal(modifiers)) {
continue;
}
if (keys.contains(field.getName())) {
field.setAccessible(true);
field.set(operation, map.get(field.getName()));
}
}
} catch (Exception e) {
// if the script contains wrong variables, and it causes exceptions, or there
// is any other problem, we just ignore it, and the original command will be executed
}
}
public static boolean isWindows() {
final String os = System.getProperty("os.name").toLowerCase();
return (os.indexOf("win") >= 0);
}
private static class StreamGobbler extends Thread {
InputStream is;
OutputStream out;
StreamGobbler(final InputStream is, final OutputStream out) {
this.is = is;
this.out = out;
setDaemon(true);
}
@Override
public void run() {
try {
int c;
while ((c = is.read()) != -1) {
out.write(c);
}
} catch (final IOException ioe) {
ioe.printStackTrace();
}
}
}
public static CommandHook createScriptHook(final File file, final boolean preHook) {
final String filename = file.getName();
final String ext = Files.getFileExtension(filename);
final File preScript = preHook ? file : null;
final File postScript = preHook ? null : file;
final CommandHook hook;
final ScriptEngine engine = factory.getEngineByExtension(ext);
if (engine == null) {
hook = new ShellScriptHook(preScript, postScript);
} else {
hook = new JVMScriptHook(preScript, postScript);
}
return hook;
}
}