/*******************************************************************************
* Copyright (c) 2012 Pivotal Software, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.grails.ide.eclipse.groovy.debug.core.evaluation;
import groovy.lang.Binding;
import groovy.lang.GroovyCodeSource;
import groovy.lang.MetaClass;
import groovy.lang.Script;
import groovy.util.Proxy;
import java.net.MalformedURLException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IImportContainer;
import org.eclipse.jdt.core.IImportDeclaration;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageDeclaration;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.Message;
import org.eclipse.jdt.debug.core.IEvaluationRunnable;
import org.eclipse.jdt.debug.core.IJavaDebugTarget;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaReferenceType;
import org.eclipse.jdt.debug.core.IJavaStackFrame;
import org.eclipse.jdt.debug.core.IJavaThread;
import org.eclipse.jdt.debug.core.IJavaValue;
import org.eclipse.jdt.debug.eval.IEvaluationEngine;
import org.eclipse.jdt.debug.eval.IEvaluationListener;
import org.eclipse.jdt.debug.eval.IEvaluationResult;
import org.eclipse.jdt.internal.debug.core.JDIDebugPlugin;
import org.eclipse.jdt.internal.debug.core.JavaDebugUtils;
import org.eclipse.jdt.internal.debug.core.model.JDIThisVariable;
import org.grails.ide.eclipse.groovy.debug.core.GroovyDebugCoreActivator;
import com.sun.jdi.InvocationException;
/**
* Evaluates a Groovy snippet in the context
* of a stepping application. This class creates a
* Groovy script based on the snippet. The script's {@link MetaClass}and {@link Binding} are set to a JDI variant that communicates
* with the application being debugged.
* @author Andrew Eisenberg
* @since 2.5.1
*/
public class GroovyJDIEvaluator {
public static final String SCRIPT_FILE_NAME = "____Eval.groovy";
public static final String SCRIPT_CLASS_NAME = "____Eval";
private class GroovyEvaluationResult implements IEvaluationResult {
private final IJavaValue value;
private final DebugException exception;
private final String snippet;
private final IJavaThread thread;
private String completeSnippet;
GroovyEvaluationResult(IJavaValue value,
DebugException exception, String snippet, String completeSnippet, IJavaThread thread) {
super();
this.value = value;
this.exception = exception;
this.snippet = snippet;
this.completeSnippet = completeSnippet;
this.thread = thread;
}
public IJavaValue getValue() {
return value;
}
public boolean hasErrors() {
return exception != null;
}
public Message[] getErrors() {
return exception != null ? new Message[] { new Message("(Groovy) " + exception.getLocalizedMessage(), -1), new Message("Evaluation snippet:\n" + completeSnippet, -1) } : new Message[0];
}
public String[] getErrorMessages() {
if (exception == null) {
return new String[0];
}
if (exception.getStatus().getException() instanceof InvocationException) {
return new String[] { "(Groovy) " + ((InvocationException) exception.getStatus().getException()).exception().toString(), "Evaluation snippet:\n" + completeSnippet };
}
return new String[] { "(Groovy) " + exception.getLocalizedMessage(), "Evaluation snippet:\n" + completeSnippet };
}
public String getSnippet() {
return snippet;
}
public DebugException getException() {
return exception;
}
public IJavaThread getThread() {
return thread;
}
public IEvaluationEngine getEvaluationEngine() {
return null;
}
public boolean isTerminated() {
return false;
}
}
private final IJavaProject javaProject;
private String packageName;
public GroovyJDIEvaluator(IJavaProject javaProject, IJavaDebugTarget target) {
super();
this.javaProject = javaProject;
}
public void evaluate(final String snippet, final IJavaObject object, final IJavaStackFrame frame,
final IEvaluationListener listener, final int evaluationDetail, final boolean hitBreakpoints) throws CoreException {
final IJavaThread thread = (IJavaThread) frame.getThread();
final JDITargetDelegate delegate = new JDITargetDelegate((IJavaDebugTarget) thread.getDebugTarget(), thread);
// really shouldn't queue if thread is already in the middle of an explicit evaluation
thread.queueRunnable(new Runnable() {
public void run() {
IEvaluationRunnable evalRunnable = new IEvaluationRunnable() {
public void run(IJavaThread thread, IProgressMonitor monitor)
throws DebugException {
performEvaluate(snippet, frame, listener, delegate, evaluationDetail, hitBreakpoints);
}
};
// finally, run the script
try {
thread.runEvaluation(evalRunnable, null, evaluationDetail, hitBreakpoints);
} catch (DebugException e) {
GroovyDebugCoreActivator.log(e);
}
}
});
}
protected void performEvaluate(String snippet, IJavaStackFrame frame,
IEvaluationListener listener,
JDITargetDelegate delegate, int evaluationDetail, boolean hitBreakpoints) {
Object result = null;
Throwable thrownException = null;
String completeSource = null;
try {
completeSource = createEvaluationSourceFromSnippet(snippet, frame);
JDIGroovyClassLoader loader = createClassLoader();
final Script script = convertSnippetToScript(completeSource, loader);
script.setMetaClass(new JDIMetaClass(delegate.getThis(), delegate));
script.setBinding(createBinding(frame, delegate));
script.getBinding().setMetaClass(script.getMetaClass());
delegate.initialize(loader, packageName + SCRIPT_CLASS_NAME);
result = script.run();
} catch (Exception e) {
// only print to sysout when this is an explicit evaluation
if (DebugEvent.EVALUATION == evaluationDetail) {
System.out.println("Exception during evaluation:");
e.printStackTrace();
}
if (e.getCause() instanceof DebugException) {
thrownException = e.getCause();
} else {
thrownException = e;
}
thrownException = new Exception("(Groovy) Complete snippet:\n" + completeSource, e);
} finally {
try {
IEvaluationResult evalResult = createEvalResult(snippet, completeSource, result, delegate, thrownException, evaluationDetail);
if (JDIDebugPlugin.getDefault() != null) {
listener.evaluationComplete(evalResult);
}
} finally {
// replace original metaclass
delegate.cleanup();
}
}
}
private String createEvaluationSourceFromSnippet(String snippet, IJavaStackFrame frame) throws CoreException {
StringBuffer sb = new StringBuffer();
sb.append("/////start\n");
IJavaReferenceType jdiType = frame.getReferenceType();
IType iType = JavaDebugUtils.resolveType(jdiType);
// could be a closure type that doesn't exist in source
if (iType != null && !iType.exists() && iType.getParent().getElementType() == IJavaElement.TYPE) {
iType = (IType) iType.getParent();
}
if (iType != null && !iType.isInterface()) {
ITypeRoot root = iType.getTypeRoot();
if (root instanceof ICompilationUnit) {
// really, a GroovyCompilationUnit
ICompilationUnit unit = (ICompilationUnit) root;
// package statement
IPackageDeclaration[] pDecls = unit.getPackageDeclarations();
if (pDecls.length > 0) {
sb.append("package " + pDecls[0].getElementName() + ";\n");
packageName = pDecls[0].getElementName() + ".";
} else {
packageName = "";
}
// imports
IImportContainer container = unit.getImportContainer();
if (container != null && container.exists()) {
IJavaElement[] children = container.getChildren();
for (int j = 0; j < children.length; j++) {
IImportDeclaration importChild = (IImportDeclaration) children[j];
sb.append("import ");
if (Flags.isStatic(importChild.getFlags())) {
sb.append("static ");
}
sb.append(importChild.getElementName());
if (importChild.isOnDemand() && ! (importChild.getElementName().endsWith(".*"))) {
sb.append(".*");
}
sb.append(";\n");
}
}
// types...create stubs for the types just so that they can be instantiated and referenced
IType[] allTypes = unit.getAllTypes();
for (IType otherType : allTypes) {
if (!otherType.equals(iType)) {
if (otherType.isInterface()) {
sb.append("interface ");
} else if (otherType.isAnnotation()) {
// probably don't need this
sb.append("@interface ");
} else if (otherType.isEnum()) {
sb.append("enum ");
} else {
sb.append("class ");
}
// use '$' so that inner classes can be remembered
String qualifiedTypeName = otherType.getFullyQualifiedName('$');
int dotIndex = qualifiedTypeName.lastIndexOf('.')+1;
String simpleName = qualifiedTypeName.substring(dotIndex);
sb.append(simpleName + "{ }\n");
}
}
}
}
sb.append(snippet);
sb.append("\n/////end");
return sb.toString();
}
private IEvaluationResult createEvalResult(String snippet, String completeSource, Object result, JDITargetDelegate delegate, Throwable thrownException, int evaluationDetail) {
if (thrownException == null) {
try {
IJavaValue jdiResult;
if (result instanceof Proxy) {
jdiResult = (IJavaValue) ((Proxy) result).getAdaptee();
} else if (result instanceof IJavaValue) {
jdiResult = (IJavaValue) result;
} else if (result == null) {
jdiResult = delegate.getTarget().nullValue();
} else {
// might be a constant expression
jdiResult = delegate.toJDIObject(result);
}
return new GroovyEvaluationResult(jdiResult, null, snippet, completeSource, delegate.getThread());
} catch (DebugException de) {
thrownException = de;
}
}
// if we get to this point, then we have an exception
DebugException debugException;
if (! (thrownException instanceof DebugException)) {
debugException = new DebugException(new Status(IStatus.ERROR, JDIDebugPlugin.getUniqueIdentifier(),
"An exception occurred durring evaluation.", thrownException));
} else {
debugException = (DebugException) thrownException;
}
if (DebugEvent.EVALUATION == evaluationDetail) {
// only log events from explicitly invoked evaluations
GroovyDebugCoreActivator.log(debugException);
}
return new GroovyEvaluationResult(delegate.getTarget().newValue("See error log: " + thrownException.getLocalizedMessage()), debugException, snippet, completeSource, delegate.getThread());
}
private Binding createBinding(IJavaStackFrame frame, JDITargetDelegate delegate) throws DebugException {
IVariable[] vars = frame.getVariables();
JDIBinding binding = new JDIBinding(delegate, frame);
for (int i = 0; i < vars.length; i++) {
if (! (vars[i] instanceof JDIThisVariable)) {
binding.setVariable(vars[i].getName(), delegate.createProxyFor((IJavaValue) vars[i].getValue()));
}
}
// add a JDIComparator for invoking comparisons
JDIComparator comparator = new JDIComparator(delegate);
binding.setProperty("__comparator", comparator);
binding.markAsInitialized();
return binding;
}
private Script convertSnippetToScript(final String completeSource, JDIGroovyClassLoader loader) throws CoreException {
GroovyCodeSource gcs = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() {
public GroovyCodeSource run() {
return new GroovyCodeSource(completeSource, getCompilationUnitName(), /*GroovyShell.DEFAULT_CODE_BASE*/ "/groovy/shell");
}
});
return InvokerHelper.createScript(loader.parseClass(gcs, false), new Binding());
}
/**
* Must create a separate, unparented class loader with the same classpath as the
* debugged application for 2 reasons:
* 1. So that all metaclasses we set do not affect the types loaded by
* other class loaders
* 2. So that we can load classes from the debugged application
*
* However, if this debugged application does something funky with its classpath (eg as in grails),
* then we will not catch it here.
* @return
* @throws CoreException
*/
private JDIGroovyClassLoader createClassLoader() throws CoreException {
JDIGroovyClassLoader groovyLoader = new JDIGroovyClassLoader();
IJavaProject currentProject = this.javaProject;
addClasspathEntries(groovyLoader, currentProject, true);
return groovyLoader;
}
private void addClasspathEntries(JDIGroovyClassLoader groovyLoader,
IJavaProject currentProject, boolean includeAll) throws JavaModelException,
CoreException {
IClasspathEntry[] entries = currentProject.getResolvedClasspath(true);
IPath workspaceLocation = ResourcesPlugin.getWorkspace().getRoot().getLocation();
for (int i = 0; i < entries.length; i++) {
if (!includeAll && !entries[i].isExported()) {
continue;
}
switch (entries[i].getEntryKind()) {
case IClasspathEntry.CPE_LIBRARY:
try {
groovyLoader.addURL(entries[i].getPath().toFile().toURL());
} catch (MalformedURLException e) {
throw new CoreException(new Status(IStatus.ERROR, GroovyDebugCoreActivator.PLUGIN_ID, e.getLocalizedMessage(), e));
}
break;
case IClasspathEntry.CPE_SOURCE:
IPath outLocation = entries[i].getOutputLocation();
if (outLocation != null) {
// using non-default output location
try {
groovyLoader.addURL(workspaceLocation.append(outLocation).toFile().toURL());
} catch (MalformedURLException e) {
throw new CoreException(new Status(IStatus.ERROR, GroovyDebugCoreActivator.PLUGIN_ID, e.getLocalizedMessage(), e));
}
}
break;
case IClasspathEntry.CPE_PROJECT:
IProject otherProject = ResourcesPlugin.getWorkspace().getRoot().getProject(entries[i].getPath().lastSegment());
if (otherProject.isAccessible()) {
IJavaProject otherJavaProject = JavaCore.create(otherProject);
addClasspathEntries(groovyLoader, otherJavaProject, false);
}
break;
default:
break;
}
}
// now add default out location
IPath outLocation = currentProject.getOutputLocation();
if (outLocation != null) {
try {
groovyLoader.addURL(workspaceLocation.append(outLocation).toFile().toURL());
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
private String getCompilationUnitName() {
return SCRIPT_FILE_NAME;
}
}