package de.gaalop.maple.engine;
import com.sun.jna.Platform;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.List;
/**
* This class wraps the maple engine so it can be used without linking errors.
*
* @author Sebastian
*/
class MapleEngineImpl implements MapleEngine {
private Log log = LogFactory.getLog(MapleEngineImpl.class);
/**
* This class is an implementation of the OpenMaple EngineCallBack interface based on the Java Reflection {@link Proxy} class.
* It forwards the text and error callbacks to the outer class.
*
* @author Sebastian
*/
private final class EngineCallback implements InvocationHandler {
@SuppressWarnings("boxing")
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("queryInterrupt") || methodName.equals("redirectCallBack")) {
return false;
} else if (methodName.equals("errorCallBack")) {
onError(args[2].toString(), (Integer) args[1]);
} else if (methodName.equals("textCallBack")) {
onText(args[2].toString(), (Integer) args[1]);
}
return null;
}
}
private static final String MAPLE_CALLBACK_INTERFACE = "com.maplesoft.openmaple.EngineCallBacks";
private static final String MAPLE_ENGINE_CLASS = "com.maplesoft.openmaple.Engine";
private Class<?> mapleCallbackInterface;
private Constructor<?> engineConstructor;
private Method evaluateMethod;
private List<String> errorMessages = new ArrayList<String>();
private Object engine;
/**
* This field is used to buffer the output from Maple that is sent via the {@link #onText(String, int)} callback.
*/
private StringBuffer outputBuffer = new StringBuffer();
/**
* This callback is called when Maple indicates an error.
*
* @param text The text describing the error.
* @param type
*/
private void onError(String text, int type) {
errorMessages.add(text);
}
/**
* This callback is called when the Maple engine outputs text.
*
* @param text The text that was sent by Maple.
* @param type
*/
private void onText(String text, int type) {
outputBuffer.append(text);
outputBuffer.append('\n');
}
/**
* Constructs a new Maple Engine. Please note that OpenMaple restrictions mandate that there may only be ONE engine at a time.
* That is why this class is managed as a singleton by {@link Maple}.
*
* @param mapleClassLoader This class loader will be queried for the important classes.
* @throws IllegalArgumentException If the engine could not be initialized by using the given class loader.
*/
MapleEngineImpl(MapleClassLoader mapleClassLoader) throws IllegalArgumentException {
try {
// Check that important classes are available
Class<?> mapleEngineClass = mapleClassLoader.loadClass(MAPLE_ENGINE_CLASS);
mapleCallbackInterface = mapleClassLoader.loadClass(MAPLE_CALLBACK_INTERFACE);
// Find Engine constructor in Engine class
engineConstructor = mapleEngineClass.getConstructor(String[].class, mapleCallbackInterface, Object.class,
Object.class);
// Find evaluate method in Engine class
evaluateMethod = mapleEngineClass.getDeclaredMethod("evaluate", String.class);
} catch (Exception e) {
throw new IllegalArgumentException("Given class loader is unable to load a valid Maple engine.", e);
}
Object callback = createEngineCallback();
engine = createEngine(callback);
}
/**
* Creates an object implementing the OpenMaple EngineCallBack interface using Java Proxies using the inner class
* {@link EngineCallback}.
*
* @return A new object that implements the Maple EngineCallBack interface and forwards all calls to an {@link EngineCallback}
* instance.
*/
private Object createEngineCallback() {
Object callbackProxy = Proxy.newProxyInstance(mapleCallbackInterface.getClassLoader(), new Class<?>[] {
mapleCallbackInterface
}, new EngineCallback());
return callbackProxy;
}
/**
* Creates an OpenMaple engine object and connects it to the given callback.
*
* @param callback
*/
private Object createEngine(Object callback) {
try {
return engineConstructor.newInstance(new String[] {
"java"
}, callback, null, null);
} catch (Exception e) {
throw new RuntimeException("Unable to instantiate Maple engine.", e);
}
}
@Override
public String evaluate(String command) throws MapleEngineException {
log.debug("Executing " + command);
errorMessages.clear();
outputBuffer.setLength(0);
try {
Object mapleResult = evaluateMethod.invoke(engine, command);
if (mapleResult != null) {
disposeMapleObject(mapleResult);
}
String result = outputBuffer.toString();
outputBuffer.setLength(0);
if (!errorMessages.isEmpty()) {
String errorMessage = "";
for (String message : errorMessages) {
errorMessage += message;
}
throw new MapleEngineException(errorMessage, command);
}
log.debug("Result: " + result);
return result;
} catch (Exception e) {
throw new MapleEngineException("Unable to execute maple commands.", command, e);
}
}
@Override
public void reset() throws MapleEngineException {
evaluate("restart:");
}
@Override
public void loadModule(InputStream input) throws MapleEngineException {
try {
File tempFile = File.createTempFile("gaalop", ".m");
try {
FileOutputStream output = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
output.close();
if (Platform.isWindows()) {
evaluate("read " + "\"" + tempFile.getAbsolutePath().replace("\\", "\\\\") + "\";");
} else {
evaluate("read \"" + tempFile.getAbsolutePath() + "\";");
}
} finally {
tempFile.delete();
}
} catch (IOException e) {
throw new MapleEngineException("Unable to load module in Maple.", e);
}
}
/**
* Calls the dispose method (if available) on the given Maple object. This method fails silently if there is no such method.
*
* @param obj An object with a dispose method. Must not be null.
*/
private void disposeMapleObject(Object obj) {
try {
Method dispose = obj.getClass().getMethod("dispose");
dispose.invoke(obj);
} catch (SecurityException e) {
log.warn("Unable to get dispose method of object.", e);
} catch (NoSuchMethodException e) {
log.warn("Unable to get dispose method of object because it doesnt exist.", e);
} catch (IllegalArgumentException e) {
log.warn("Unable to call dispose method because of an arguments mismatch.", e);
} catch (IllegalAccessException e) {
log.warn("Unable to call dispose method because it is not accessible.", e);
} catch (InvocationTargetException e) {
log.warn("Unable to call dispose method.", e);
}
}
}