package com.mobilesorcery.sdk.html5.debug;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.wst.jsdt.core.compiler.IProblem;
import org.eclipse.wst.jsdt.core.dom.AST;
import org.eclipse.wst.jsdt.core.dom.ASTNode;
import org.eclipse.wst.jsdt.core.dom.ASTParser;
import org.eclipse.wst.jsdt.core.dom.ASTVisitor;
import org.eclipse.wst.jsdt.core.dom.Block;
import org.eclipse.wst.jsdt.core.dom.BodyDeclaration;
import org.eclipse.wst.jsdt.core.dom.CatchClause;
import org.eclipse.wst.jsdt.core.dom.DoStatement;
import org.eclipse.wst.jsdt.core.dom.ForInStatement;
import org.eclipse.wst.jsdt.core.dom.ForStatement;
import org.eclipse.wst.jsdt.core.dom.FunctionDeclaration;
import org.eclipse.wst.jsdt.core.dom.IfStatement;
import org.eclipse.wst.jsdt.core.dom.JSdoc;
import org.eclipse.wst.jsdt.core.dom.JavaScriptUnit;
import org.eclipse.wst.jsdt.core.dom.LabeledStatement;
import org.eclipse.wst.jsdt.core.dom.SimpleName;
import org.eclipse.wst.jsdt.core.dom.SingleVariableDeclaration;
import org.eclipse.wst.jsdt.core.dom.Statement;
import org.eclipse.wst.jsdt.core.dom.SwitchCase;
import org.eclipse.wst.jsdt.core.dom.SwitchStatement;
import org.eclipse.wst.jsdt.core.dom.ThisExpression;
import org.eclipse.wst.jsdt.core.dom.VariableDeclarationFragment;
import org.eclipse.wst.jsdt.core.dom.WhileStatement;
import org.eclipse.wst.jsdt.core.dom.WithStatement;
import org.eclipse.wst.jsdt.debug.core.breakpoints.IJavaScriptLineBreakpoint;
import org.eclipse.wst.jsdt.debug.core.model.JavaScriptDebugModel;
import org.eclipse.wst.jsdt.web.core.javascript.JsTranslator;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.xml.core.internal.parser.regions.TagNameRegion;
import org.eclipse.wst.xml.core.internal.text.XMLStructuredDocumentRegion;
import com.mobilesorcery.sdk.core.CoreMoSyncPlugin;
import com.mobilesorcery.sdk.core.IFileTreeDiff;
import com.mobilesorcery.sdk.core.IFilter;
import com.mobilesorcery.sdk.core.MoSyncBuilder;
import com.mobilesorcery.sdk.core.MoSyncProject;
import com.mobilesorcery.sdk.core.Pair;
import com.mobilesorcery.sdk.core.Util;
import com.mobilesorcery.sdk.core.templates.Template;
import com.mobilesorcery.sdk.html5.Html5Plugin;
import com.mobilesorcery.sdk.html5.debug.hotreplace.FileRedefinable;
import com.mobilesorcery.sdk.html5.debug.hotreplace.FunctionRedefinable;
import com.mobilesorcery.sdk.html5.debug.hotreplace.HTMLRedefinable;
import com.mobilesorcery.sdk.html5.debug.hotreplace.ProjectRedefinable;
import com.mobilesorcery.sdk.html5.debug.rewrite.CatchRewrite;
import com.mobilesorcery.sdk.html5.debug.rewrite.FunctionRewrite;
import com.mobilesorcery.sdk.html5.debug.rewrite.ISourceSupport;
import com.mobilesorcery.sdk.html5.debug.rewrite.NodeRewrite;
import com.mobilesorcery.sdk.html5.debug.rewrite.SourceRewrite;
import com.mobilesorcery.sdk.html5.debug.rewrite.StatementRewrite;
import com.mobilesorcery.sdk.html5.debug.rewrite.ThisRewrite;
public class JSODDSupport {
public class InvalidASTException extends RuntimeException {
public InvalidASTException(String msg) {
super(msg);
}
}
/**
* A constant representing the 'drop to frame' feature.
* @see Html5Plugin#isFeatureSupported(String)
*/
public static final String DROP_TO_FRAME = "drop.to.frame";
/**
* A constant representing the 'edit-and-continue' feature.
* @see Html5Plugin#isFeatureSupported(String)
*/
public static final String EDIT_AND_CONTINUE = "edit.continue";
/**
* A constant representing the 'line breakpoints' feature.
* @see Html5Plugin#isFeatureSupported(String)
*/
public static final String LINE_BREAKPOINTS = "line.breakpoint";
/**
* A constant representing the 'artificial stack' feature.
* @see Html5Plugin#isFeatureSupported(String)
*/
public static final String ARTIFICIAL_STACK = "artificial.stack";
public static final String EVAL_FUNC_SNIPPET = "function(____eval) {return eval(____eval);}";
// TODO: Now we always keep the entire source tree in memory -- not
// necessarily a good thing...
public class DebugRewriteOperationVisitor extends ASTVisitor implements
ISourceSupport {
private static final int INSTRUMENTATION_DISALLOWED = 0;
private static final int INSTRUMENTATION_ALLOWED = 1;
private static final int FORCE_INSTRUMENTATION = 2;
private static final int INSTRUMENTATION_BLACKLISTED = 3;
private static final int ERROR_COUNT_THRESHOLD = 10;
private JavaScriptUnit unit;
private LocalVariableScope currentScope = new LocalVariableScope()
.nestScope();
private final Stack<FunctionRewrite> functionRewriteStack = new Stack<FunctionRewrite>();
private final Stack<IRedefinable> redefinableStack = new Stack<IRedefinable>();
private final TreeMap<Integer, LocalVariableScope> localVariables = new TreeMap<Integer, LocalVariableScope>();
private final HashSet<ASTNode> exclusions = new HashSet<ASTNode>();
private final HashSet<ASTNode> blockifiables = new HashSet<ASTNode>();
private final TreeMap<Integer, List<String>> insertions = new TreeMap<Integer, List<String>>();
private final HashMap<ASTNode, JavaScriptUnit> nodeToUnitMap = new HashMap<ASTNode, JavaScriptUnit>();
private final HashMap<ASTNode, String> nodeRedefinables = new HashMap<ASTNode, String>();
private final HashMap<String, IRedefinable> redefinables = new HashMap<String, IRedefinable>();
private TreeSet<Integer> scopeResetPoints;
private String instrumented;
private HashMap<ASTNode, NodeRewrite> rewrites = new HashMap<ASTNode, NodeRewrite>();
private String originalSource;
private long fileId;
private HashMap<Integer, NodeRewrite> instrumentedLines = new HashMap<Integer, NodeRewrite>();
private LineMap lineMap;
private Position currentPosition;
public DebugRewriteOperationVisitor(LineMap lineMap, long fileId) {
this.lineMap = lineMap;
this.fileId = fileId;
}
@Override
public void preVisit(ASTNode node) {
currentPosition = getPosition(node, true);
int start = getStartPosition(node);
int scopeStart = start;
int startLine = unit == null ? -1 : unit.getLineNumber(start);
LocalVariableScope startScope = currentScope;
Integer scopeResetPoint = scopeResetPoints.floor(start);
if (scopeResetPoint != null) {
scopeResetPoints.remove(scopeResetPoint);
currentScope = currentScope.clear();
scopeStart = scopeResetPoint;
}
boolean nest = isNestStatement(node);
blockify(node);
if (node instanceof FunctionDeclaration) {
FunctionDeclaration fd = (FunctionDeclaration) node;
currentScope = currentScope.nestScope();
// Special function var.
currentScope = currentScope.addLocalVariableDeclaration("arguments");
for (Object paramObj : fd.parameters()) {
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
String name = param.getName().getIdentifier();
currentScope = currentScope
.addLocalVariableDeclaration(name);
}
SimpleName functionName = fd.getName();
boolean isAnonymous = isAnonymous(fd);
String functionIdentifier = isAnonymous ? Html5Plugin.ANONYMOUS_FUNCTION
: functionName.getIdentifier();
FunctionRewrite functionRewrite = new FunctionRewrite(this, fd, fileId,nodeRedefinables);
rewrites.put(fd, functionRewrite);
functionRewriteStack.push(functionRewrite);
Block body = fd.getBody();
ASTNode firstStatement = startOfFunction(fd);
ASTNode lastStatement = endOfFunction(fd);
// TODO: If both first and last statement null, then we have a problem
// and should blacklist that node.
if (firstStatement instanceof Block
&& lastStatement instanceof Block) {
forceBlockify(body);
}
// Add this to the set of redefinables
pushRedefinable(new FunctionRedefinable(currentRedefinable(),
this, node), fd);
// Already nested!
nest = false;
}
if (node instanceof CatchClause) {
rewrites.put(node, new CatchRewrite(this, node));
}
checkForExclusion(node);
checkBlacklist(node);
if (node instanceof JavaScriptUnit) {
unit = (JavaScriptUnit) node;
validateAST(unit.getProblems());
}
int instrumentable = isInstrumentableStatement(node);
if (instrumentable != INSTRUMENTATION_DISALLOWED) {
rewrites.put(node, new StatementRewrite(this, node, fileId,
localVariables, blockifiables, instrumentedLines,
instrumentable == FORCE_INSTRUMENTATION));
}
if (node instanceof ThisExpression && !functionRewriteStack.isEmpty()) {
rewrites.put(node, new ThisRewrite(this, node));
functionRewriteStack.peek().useEscapedThis(true);
}
if (nest) {
currentScope = currentScope.nestScope();
}
if (currentScope != startScope) {
localVariables.put(scopeStart, currentScope);
}
}
private void checkForExclusion(ASTNode node) {
if (node instanceof SwitchStatement) {
// The first statement in a switch statement
// should always be a case-switch statement.
SwitchStatement switchStatement = (SwitchStatement) node;
List statements = switchStatement.statements();
for (Object statementObj : statements) {
if (statementObj instanceof SwitchCase) {
addToExclusionList((SwitchCase) statementObj);
continue;
}
}
}
if (node instanceof LabeledStatement) {
// Labels aren't really executable, but their statements are.
LabeledStatement labeledStatement = (LabeledStatement) node;
addToExclusionList(labeledStatement.getBody());
}
if (node instanceof ForInStatement) {
ForInStatement forInStatement = (ForInStatement) node;
addToExclusionList(forInStatement.getIterationVariable());
}
}
private void checkBlacklist(ASTNode node) {
}
private ASTNode startOfFunction(FunctionDeclaration fd) {
Block body = fd.getBody();
if (body == null) {
return null;
}
List statements = body.statements();
boolean useBody = statements.isEmpty();
return (ASTNode) (useBody ? body : statements.get(0));
}
private ASTNode endOfFunction(FunctionDeclaration fd) {
Block body = fd.getBody();
if (body == null) {
return null;
}
List statements = body.statements();
boolean useBody = statements.isEmpty();
return (ASTNode) (useBody ? body : statements
.get(statements.size() - 1));
}
private Position functionPosition(ASTNode node, boolean start) {
boolean emptyBody = node.getParent() instanceof FunctionDeclaration;
Position pos = getPosition(node, start && !emptyBody);
// We always use the last }, since there is no comments or
// anything that can ruin or position.
int offset = emptyBody ? -1 : 0;
throw new IllegalArgumentException("!agdfgfdlj");
}
private boolean isAnonymous(FunctionDeclaration fd) {
return fd.getName() == null;
}
@Override
public void postVisit(ASTNode node) {
currentPosition = getPosition(node, false);
LocalVariableScope startScope = currentScope;
boolean unnest = isNestStatement(node);
if (node instanceof FunctionDeclaration) {
popRedefinable();
functionRewriteStack.pop();
}
if (node instanceof VariableDeclarationFragment) {
VariableDeclarationFragment localVar = (VariableDeclarationFragment) node;
String name = localVar.getName().getIdentifier();
currentScope = currentScope.addLocalVariableDeclaration(name);
}
if (node instanceof JavaScriptUnit) {
unit = null;
}
if (unnest) {
currentScope = currentScope.unnestScope();
}
if (currentScope != startScope) {
int end = getStartPosition(node) + getLength(node);
localVariables.put(end, currentScope);
}
exclusions.remove(node);
}
private boolean isNestStatement(ASTNode node) {
// No block scope in JS.
return node instanceof FunctionDeclaration;
}
private int getStartPosition(ASTNode node) {
int pos = node.getStartPosition();
// The JavaScriptDoc is always the first you get.
int docLength = node.getLength() - getLength(node);
return pos + docLength;
}
private int getLength(ASTNode node) {
int docLength = 0;
if (node instanceof BodyDeclaration) {
BodyDeclaration bodyDeclaration = (BodyDeclaration) node;
JSdoc doc = bodyDeclaration.getJavadoc();
if (doc != null) {
docLength = doc.getLength();
}
}
return node.getLength() - docLength;
}
private boolean shouldBlockify(ASTNode node) {
return blockifiables.contains(node);
}
private void forceBlockify(ASTNode node) {
blockifiables.add(node);
}
private void blockify(ASTNode node) {
if (node instanceof IfStatement) {
IfStatement ifStatement = (IfStatement) node;
addToBlockifyList(ifStatement.getThenStatement());
addToBlockifyList(ifStatement.getElseStatement());
} else if (node instanceof ForStatement) {
ForStatement forStatement = (ForStatement) node;
addToBlockifyList(forStatement.getBody());
} else if (node instanceof ForInStatement) {
ForInStatement forInStatement = (ForInStatement) node;
addToBlockifyList(forInStatement.getBody());
} else if (node instanceof WithStatement) {
WithStatement withStatement = (WithStatement) node;
addToBlockifyList(withStatement.getBody());
} else if (node instanceof WhileStatement) {
WhileStatement whileStatement = (WhileStatement) node;
addToBlockifyList(whileStatement.getBody());
} else if (node instanceof DoStatement) {
DoStatement doStatement = (DoStatement) node;
addToBlockifyList(doStatement.getBody());
}
}
private void addToBlockifyList(ASTNode statement) {
if (statement == null || statement instanceof Block) {
// Already blockified!
return;
}
blockifiables.add(statement);
}
private void addToExclusionList(ASTNode node) {
if (node != null) {
exclusions.add(node);
}
}
private void addStatementInstrumentationLocation(JavaScriptUnit unit,
ASTNode node,
HashMap<Integer, List<Pair<ASTNode, Boolean>>> lineMap,
boolean before) {
if (unit == null) {
return;
}
nodeToUnitMap.put(node, unit);
Position pos = getPosition(node, before);
int insertionLine = pos.getLine();
int insertionCol = pos.getColumn();
List<Pair<ASTNode, Boolean>> nodeList = lineMap.get(insertionLine);
if (nodeList == null) {
nodeList = new ArrayList<Pair<ASTNode, Boolean>>();
lineMap.put(insertionLine, nodeList);
}
if (insertionLine > 0 && insertionCol >= 0) {
nodeList.add(new Pair(node, before));
}
}
private int isInstrumentableStatement(ASTNode node) {
if (exclusions.contains(node)) {
return INSTRUMENTATION_DISALLOWED;
}
boolean isStatement = node instanceof Statement;
boolean isBlock = node instanceof Block;
// We should be able to break in empty function blocks.
boolean isEmptyFunctionBlock = isBlock
&& ((Block) node).statements().isEmpty()
&& node.getParent() instanceof FunctionDeclaration;
if (isEmptyFunctionBlock) {
return FORCE_INSTRUMENTATION;
}
boolean allowInstrumentation = (isStatement && !isBlock);
return allowInstrumentation ? INSTRUMENTATION_ALLOWED
: INSTRUMENTATION_DISALLOWED;
}
private void insert(Position position, String text) {
int pos = position.getPosition();
List<String> insertionsForPosition = insertions.get(pos);
if (insertionsForPosition == null) {
insertionsForPosition = new ArrayList<String>();
insertions.put(pos, insertionsForPosition);
}
insertionsForPosition.add(text);
}
private void block(Position position, boolean start) {
insert(position, "\n" + (start ? '{' : '}') + "\n");
}
public void rewrite(long fileId, String originalSource, int fwImportLocation, Writer output,
NavigableMap<Integer, LocalVariableScope> scopeMap,
NavigableSet<Integer> instrumentedLines) throws IOException, CoreException {
// TODO: I guess there are some better AST rewrite methods around.
// Or... never mind, I've invested way too much time in this :)
LineMap lineByLineOriginalSource = new LineMap(originalSource);
this.originalSource = originalSource;
for (Map.Entry<Integer, LocalVariableScope> scope : localVariables
.entrySet()) {
int mappedLineNo = lineByLineOriginalSource.getLine(scope
.getKey());
scopeMap.put(mappedLineNo, scope.getValue());
}
NodeRewrite rootRewrite = new NodeRewrite(this, null);
// Collect rewrites
for (ASTNode rewrittenNode : rewrites.keySet()) {
NodeRewrite parentRewrite = getClosestRewriteAncestor(rewrittenNode);
if (parentRewrite == null) {
parentRewrite = rootRewrite;
}
parentRewrite.addChild(rewrites.get(rewrittenNode));
}
SourceRewrite doc = new SourceRewrite(originalSource);
if (fwImportLocation > 0) {
doc.seek(fwImportLocation);
doc.insert(generateFrameworkImport());
}
rootRewrite.rewrite(null, doc);
instrumented = doc.rewrite();
instrumentedLines.addAll(this.instrumentedLines.keySet());
if (output != null) {
output.write(instrumented);
}
}
private NodeRewrite getClosestRewriteAncestor(ASTNode node) {
ASTNode parent = node.getParent();
if (parent == null) {
return null;
}
NodeRewrite parentRewrite = rewrites.get(parent);
if (parentRewrite == null) {
return getClosestRewriteAncestor(parent);
}
return parentRewrite;
}
public Position getPosition(ASTNode node, boolean start) {
JavaScriptUnit unit = (JavaScriptUnit) node.getRoot();
if (unit == null) {
throw new IllegalStateException("Node has no matched unit");
}
return new Position(node, lineMap, start);
}
public void setScopeResetPoints(TreeSet<Integer> scopeResetPoints) {
this.scopeResetPoints = new TreeSet<Integer>(scopeResetPoints);
}
private void pushRedefinable(IRedefinable redefinable, ASTNode node) {
redefinables.put(redefinable.key(), redefinable);
if (node != null) {
nodeRedefinables.put(node, redefinable.key());
}
redefinableStack.push(redefinable);
}
private IRedefinable popRedefinable() {
return redefinableStack.pop();
}
private IRedefinable currentRedefinable() {
return redefinableStack.peek();
}
@Override
public String getInstrumentedSource(IFilter<String> features,
ASTNode node) {
NodeRewrite rewrite = rewrites.get(node);
if (rewrite == null) {
// TODO
throw new RuntimeException("Could not find rewrite for node.");
}
SourceRewrite doc = new SourceRewrite(originalSource, node);
rewrite.rewrite(features, doc);
return doc.rewrite();
/*
* Position startPos = getPosition(node, true); Position endPos =
* getPosition(node, false); int start = startPos.getPosition(); int
* end = endPos.getPosition(); Entry<Integer, Integer>
* startDeltaEntry = movedSourceMap .floorEntry(start - 1);
* Entry<Integer, Integer> endDeltaEntry = movedSourceMap
* .floorEntry(end); int startDelta = startDeltaEntry == null ? 0 :
* startDeltaEntry .getValue(); int endDelta = endDeltaEntry == null
* ? 0 : endDeltaEntry.getValue(); return
* instrumented.substring(start + startDelta, end + endDelta);
*/
}
public String getInstrumentedSource() {
return instrumented;
}
public void setFileRedefinable(FileRedefinable file) {
pushRedefinable(file, null);
}
@Override
public String getSource(ASTNode node) {
return getSource(getPosition(node, true).getPosition(),
getPosition(node, false).getPosition());
}
@Override
public String getSource(int start, int end) {
return originalSource.substring(start, end);
}
@Override
public String getSource() {
return originalSource;
}
public void validateAST(IProblem[] problems) {
int errorCount = 0;
StringBuffer errorMsg = new StringBuffer();
for (IProblem problem : problems) {
if (problem.isError()) {
errorMsg.append('\n' + problem.getMessage());
}
}
if (errorCount > 0) {
String truncateMsg = errorCount > ERROR_COUNT_THRESHOLD ?
MessageFormat.format(" (Showing the {0} first errors.)", ERROR_COUNT_THRESHOLD) : "";
// Throw unchecked exception.
throw new InvalidASTException(
MessageFormat.format("Invalid JavaScript; found {0} errors{1}:{2}", errorCount, truncateMsg, errorMsg));
}
}
public Position getCurrentPosition() {
return currentPosition;
}
}
public static final String SERVER_HOST_PROP = "SERVER_HOST";
public static final String SERVER_PORT_PROP = "SERVER_PORT";
public static final String PROJECT_NAME_PROP = "PROJECT_NAME";
private static final Map<String, IRedefinable> EMPTY = Collections
.emptyMap();
private final ASTParser parser;
private HashMap<IPath, Long> fileIds = null;
private final TreeMap<Long, IPath> reverseFileIds = new TreeMap<Long, IPath>();
private final HashMap<Long, NavigableMap<Integer, LocalVariableScope>> scopeMaps = new HashMap<Long, NavigableMap<Integer, LocalVariableScope>>();
private final HashMap<Long, NavigableSet<Integer>> lineMaps = new HashMap<Long, NavigableSet<Integer>>();
private final HashMap<IFile, String> instrumentedSource = new HashMap<IFile, String>();
private HashMap<IPath, Map<String, IRedefinable>> redefinables = new HashMap<IPath, Map<String, IRedefinable>>();
private ProjectRedefinable projectRedefinable;
private CopyOnWriteArrayList<IRedefineListener> redefineListeners = new CopyOnWriteArrayList<IRedefineListener>();
private long currentFileId = 0;
private final IProject project;
public JSODDSupport(IProject project) {
this.project = project;
applyDiff(null);
parser = ASTParser.newParser(AST.JLS3);
}
public boolean applyDiff(IFileTreeDiff diff) {
final boolean[] result = new boolean[1];
if (diff == null) {
projectRedefinable = null;
try {
if (fileIds == null) {
fileIds = new HashMap<IPath, Long>();
}
project.accept(new IResourceVisitor() {
@Override
public boolean visit(IResource resource)
throws CoreException {
if (MoSyncBuilder.isInOutput(resource.getProject(),
resource)) {
return false;
}
IPath location = resource.getFullPath();
result[0] |= (fileIds == null || fileIds.get(location) == null);
assignFileId(location);
return true;
}
});
} catch (CoreException e) {
// Bah. Just ignore.
CoreMoSyncPlugin.getDefault().log(e);
}
} else {
Collection<IPath> added = diff.getAdded();
for (IPath path : added) {
result[0] |= (fileIds == null || fileIds.get(path) == null);
assignFileId(path);
}
}
return result[0];
}
public FileRedefinable delete(IPath filePath, ProjectRedefinable baseline) {
initProjectRedefinable();
IFile file = (IFile) ResourcesPlugin.getWorkspace().getRoot()
.findMember(filePath);
FileRedefinable fileRedefinable = new FileRedefinable(null, file, true);
if (baseline != null) {
baseline.replaceChild(fileRedefinable);
}
return fileRedefinable;
}
public void writeFramework(Writer output) throws CoreException {
IFile frameworkFile = project.getFile(Html5Plugin
.getHTML5Folder(project).append(getFrameworkPath()));
String frameworkSource = generateFrameworkSource();
this.instrumentedSource.put(frameworkFile, frameworkSource);
if (output != null) {
try {
output.write(frameworkSource);
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, Html5Plugin.PLUGIN_ID, "Cannot write debug framework", e));
}
}
}
public FileRedefinable rewrite(IPath filePath, Writer output,
ProjectRedefinable baseline) throws CoreException {
DebugRewriteOperationVisitor visitor = null;
try {
initProjectRedefinable();
IFile file = (IFile) ResourcesPlugin.getWorkspace().getRoot()
.findMember(filePath);
File absoluteFile = file.getLocation().toFile();
FileRedefinable fileRedefinable = new FileRedefinable(null, file);
if (isValidJavaScriptFile(filePath)) {
String source = Util.readFile(absoluteFile.getAbsolutePath(), "UTF8");
String prunedSource = source;
TreeSet<Integer> scopeResetPoints = new TreeSet<Integer>();
long fileId = assignFileId(filePath);
LineMap sourceLineMap = new LineMap(source);
visitor = new DebugRewriteOperationVisitor(
sourceLineMap, fileId);
int fwImportLocation = -1;
if (isEmbeddedJavaScriptFile(filePath)) {
ArrayList<Pair<Integer, Integer>> htmlRanges = new ArrayList<Pair<Integer, Integer>>();
ArrayList<Pair<Integer, Integer>> htmlImportRanges = new ArrayList<Pair<Integer, Integer>>();
Pair<String, Integer> prunedSourceAndLoc = getEmbeddedJavaScript(file,
scopeResetPoints, htmlRanges);
prunedSource = prunedSourceAndLoc.first;
fwImportLocation = prunedSourceAndLoc.second;
HTMLRedefinable htmlRedefinable = new HTMLRedefinable(null,
file, visitor);
htmlRedefinable.setHtmlRanges(htmlRanges);
fileRedefinable = htmlRedefinable;
}
// 1. Parse (JSDT)
parser.setSource(prunedSource.toCharArray());
TreeMap<Integer, LocalVariableScope> scopeMap = new TreeMap<Integer, LocalVariableScope>();
TreeSet<Integer> instrumentedLines = new TreeSet<Integer>();
try {
ASTNode ast = parser.createAST(new NullProgressMonitor());
// 2. Instrument
visitor.setFileRedefinable(fileRedefinable);
visitor.setScopeResetPoints(scopeResetPoints);
ast.accept(visitor);
} catch (Exception e) {
int errorLine = findPossibleErrorLine(visitor);
fileRedefinable.setErrorMessage(MessageFormat.format("Could not parse {0} -- probably a syntax error close to line {1}. Debugging disabled for this file.", filePath.toOSString(), errorLine));
}
visitor.rewrite(fileId, source, fwImportLocation, output, scopeMap,
instrumentedLines);
// 3. Update state and notify listeners
String instrumentedSource = visitor.getInstrumentedSource();
// 4. Do another parse of the instrumented stuff.
try {
if (fileRedefinable.validate() == null) {
parser.setSource(instrumentedSource.toCharArray());
parser.createAST(new NullProgressMonitor());
}
} catch (Exception e) {
instrumentedSource = source;
fileRedefinable.setErrorMessage(MessageFormat.format("Unable to instrument {0} due to limitiations in the instrumentation engine. Debugging disabled for this file", filePath.toOSString()));
}
this.instrumentedSource.put(file, instrumentedSource);
scopeMaps.put(fileId, scopeMap);
lineMaps.put(fileId, instrumentedLines);
}
if (baseline != null) {
baseline.replaceChild(fileRedefinable);
}
return fileRedefinable;
} catch (CoreException e) {
throw e;
} catch (Exception e) {
String positionHint = "";
int line = findPossibleErrorLine(visitor);
if (line > 0) {
positionHint = ", near line " + line;
}
String locationHintMsg = MessageFormat.format("In file {0}{1}: {2}", filePath.toOSString(), positionHint, e.getMessage());
throw new CoreException(new Status(IStatus.ERROR,
Html5Plugin.PLUGIN_ID, locationHintMsg, e));
}
}
private int findPossibleErrorLine(DebugRewriteOperationVisitor visitor) {
Position currentPosition = null;
if (visitor != null) {
currentPosition = visitor.getCurrentPosition();
}
if (currentPosition != null) {
return currentPosition.getLine();
}
return 0;
}
private void initProjectRedefinable() {
// TODO: Maybe we should let all build state be stored here!?
if (projectRedefinable == null) {
projectRedefinable = new ProjectRedefinable(project);
}
}
// Returns a string where everything that is not javascript is replaced by
// spaces. The second value is the location of the first script tag.
private Pair<String, Integer> getEmbeddedJavaScript(IFile file,
NavigableSet<Integer> scopeResetPoints, ArrayList<Pair<Integer, Integer>> htmlRanges) throws Exception {
if (!"UTF-8".equals(file.getCharset())) {
throw new IllegalArgumentException(MessageFormat.format("File {0} is not UTF-8 encoded, cannot continue", file.getLocation().toFile().getAbsolutePath()));
}
final CountDownLatch latch = new CountDownLatch(1);
IModelManager modelManager = StructuredModelManager.getModelManager();
IStructuredDocument doc = modelManager
.createStructuredDocumentFor(file);
JsTranslator translator = new JsTranslator(doc, file.getFullPath()
.toOSString()) {
@Override
public void finishedTranslation() {
super.finishedTranslation();
latch.countDown();
}
};
translator.translate();
latch.await();
// This is because we parse embedded js as one file; we need to know
// where to reset the scopes. Overly complicated...
org.eclipse.jface.text.Position[] htmlLocations = translator
.getHtmlLocations();
for (org.eclipse.jface.text.Position htmlLocation : htmlLocations) {
scopeResetPoints.add(htmlLocation.offset);
}
for (int i = 0; i <= htmlLocations.length; i++) {
int start = i == 0 ? 0 : htmlLocations[i - 1].offset
+ htmlLocations[i - 1].length;
int end = i == htmlLocations.length ? doc.getLength() : htmlLocations[i].offset;
htmlRanges.add(new Pair<Integer, Integer>(start, end));
}
// The import ranges in JSDT is (of course!) not comprehensive... so we need to roll our own.
// Again, using internal APIs.
IStructuredDocumentRegion[] regs = doc.getStructuredDocumentRegions();
int fwImportLocation = -1;
for (int i = 0; fwImportLocation == -1 && i < regs.length; i++) {
if (regs[i] instanceof XMLStructuredDocumentRegion) {
XMLStructuredDocumentRegion xmlReg = (XMLStructuredDocumentRegion) regs[i];
for (int j = 0; fwImportLocation == -1 && j < xmlReg.getRegions().size(); j++) {
if (xmlReg.getRegions().get(j) instanceof TagNameRegion) {
TagNameRegion tag = (TagNameRegion) xmlReg.getRegions().get(j);
String tagName = xmlReg.getFullText(tag);
if ("script".equalsIgnoreCase(tagName.trim())) {
fwImportLocation = xmlReg.getStart();
continue;
}
}
}
}
}
/*org.eclipse.jface.text.Position[] importRanges = translator.getImportHtmlRanges();
for (int i = 0; i < importRanges.length; i++) {
org.eclipse.jface.text.Position importRange = importRanges[i];
importHtmlRanges.add(new Pair<Integer, Integer>(importRange.offset, importRange.offset + importRange.length));
}*/
return new Pair<String, Integer>(translator.getJsText(), fwImportLocation);
}
public static boolean isValidJavaScriptFile(IPath file) {
// TODO: Use content descriptors!?
// TODO: JS embedded in HTML
return isEmbeddedJavaScriptFile(file)
|| (file != null && "js".equalsIgnoreCase(file
.getFileExtension()));
}
public static boolean isEmbeddedJavaScriptFile(IPath file) {
return file != null && "html".equalsIgnoreCase(file.getFileExtension());
}
private long assignFileId(IPath file) {
if (!isValidJavaScriptFile(file)) {
return -1;
}
if (fileIds == null) {
fileIds = new HashMap<IPath, Long>();
}
Long fileId = fileIds.get(file);
if (fileId == null) {
fileId = currentFileId;
fileIds.put(file, fileId);
reverseFileIds.put(fileId, file);
currentFileId++;
}
return fileId;
}
private IPath getFile(long fileId) {
return reverseFileIds.get(fileId);
}
public LocalVariableScope getScope(IFile file, int lineNo) {
Long fileId = fileIds.get(file.getFullPath());
if (fileId == null) {
return null;
}
NavigableMap<Integer, LocalVariableScope> scopeMap = scopeMaps
.get(fileId);
if (scopeMap == null) {
try {
// TODO: Force build instead!!!?
rewrite(file.getFullPath(), null, getBaseline());
scopeMap = scopeMaps.get(fileId);
} catch (CoreException e) {
// Gah.
}
}
if (scopeMap != null) {
Entry<Integer, LocalVariableScope> scope = scopeMap
.floorEntry(lineNo - 1);
if (scope != null) {
return scope.getValue();
}
}
return LocalVariableScope.EMPTY;
}
public String generateFrameworkImport() throws CoreException {
return MessageFormat.format(
"<script type=\"text/javascript\" charset=\"utf-8\" src=\"{0}\"></script>",
getFrameworkPath());
}
public static String getFrameworkPath() {
return "wormhole_dbg_fw.js";
}
public String generateFrameworkSource() throws CoreException {
MoSyncProject project = MoSyncProject.create(this.project);
StringWriter boilerplateOutput = new StringWriter();
writeTemplate(project, "/templates/jsoddsupport.template", new HashMap<String, String>(),
boilerplateOutput);
return boilerplateOutput.getBuffer().toString();
}
public void generateRemoteFetch(MoSyncProject project, IResource resource,
Writer remoteFetchOutput) throws CoreException {
Map<String, String> additionalProperties = new HashMap<String, String>();
if (resource instanceof IFile) {
IPath filePath = Html5Plugin.getDefault().getLocalPath((IFile) resource);
if (filePath != null) {
additionalProperties.put("FILE_PATH", filePath.toPortableString());
if (isEmbeddedJavaScriptFile(resource.getFullPath())) {
writeTemplate(project, "/templates/hcr.template", additionalProperties, remoteFetchOutput);
} else {
writeTemplate(project, "/templates/hcrjs.template", additionalProperties, remoteFetchOutput);
}
}
}
}
private void writeTemplate(MoSyncProject project, String templatePath, Map<String, String> additionalProperties,
Writer output) throws CoreException {
Template template = new Template(getClass().getResource(templatePath));
Map<String, String> properties = getTemplateProperties(project);
properties.putAll(additionalProperties);
try {
String contents = template.resolve(properties);
output.write(contents);
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR,
Html5Plugin.PLUGIN_ID, e.getMessage(), e));
}
}
private Map<String, String> getTemplateProperties(MoSyncProject project)
throws CoreException {
Map<String, String> properties = getDefaultProperties();
properties.putAll(project.getProperties());
properties.put("INIT_FILE_IDS", generateFileIdInitCode());
properties.put("PROJECT_NAME", project.getName());
properties.put("TIMEOUT_IN_MS",
Integer.toString(1000 * Html5Plugin.getDefault().getTimeout()));
return properties;
}
public static Map<String, String> getDefaultProperties() throws CoreException {
URL serverURL;
try {
serverURL = Html5Plugin.getDefault().getServerURL();
} catch (Exception e) {
throw new CoreException(new Status(IStatus.ERROR,
Html5Plugin.PLUGIN_ID,
"Could not determine localhost address"));
}
HashMap<String, String> properties = new HashMap<String, String>();
properties.put(SERVER_HOST_PROP, serverURL.getHost());
properties.put(SERVER_PORT_PROP, Integer.toString(serverURL.getPort()));
return properties;
}
private String generateFileIdInitCode() {
StringBuffer result = new StringBuffer();
for (Map.Entry<Long, IPath> entry : reverseFileIds.entrySet()) {
result.append("idToFile[" + entry.getKey() + "]=\""
+ entry.getValue().toPortableString() + "\";\n");
result.append("fileToId[\"" + entry.getValue().toPortableString()
+ "\"]=" + entry.getKey() + ";\n");
}
return result.toString();
}
public Set<IPath> getAllFiles() {
if (fileIds == null) {
applyDiff(null);
}
Set<IPath> allPaths = new HashSet<IPath>(fileIds.keySet());
return allPaths;
}
public String getInstrumentedSource(IFile file) {
return instrumentedSource.get(file);
}
/**
* Returns the best matching breakpoint for a specific file/line pair.
*
* @param path
* @param hitLine
* @return
*/
public static IJavaScriptLineBreakpoint findBreakPoint(IPath path, int hitLine) {
IBreakpoint[] bps = DebugPlugin.getDefault().getBreakpointManager()
.getBreakpoints(JavaScriptDebugModel.MODEL_ID);
IJavaScriptLineBreakpoint closest = null;
for (IBreakpoint bp : bps) {
if (bp instanceof IJavaScriptLineBreakpoint) {
IJavaScriptLineBreakpoint lineBp = (IJavaScriptLineBreakpoint) bp;
try {
if (Util.equals(path, new Path(lineBp.getScriptPath()))) {
int closestLine = closest == null ? 0 : closest
.getLineNumber();
int bpLine = lineBp.getLineNumber();
// We will try to get as close as possible to the hit
// line
// but never *after* it.
if (bpLine > closestLine && bpLine <= hitLine) {
closest = (IJavaScriptLineBreakpoint) bp;
}
}
} catch (CoreException e) {
// Just IGNORE!
}
}
}
return closest;
}
/**
* <p>
* Not all lines are actually instrumented for line breakpoints. This method
* will find the best matching line that has been instrumented for
* breakpoints.
* </p>
* <p>
* The best matching line will always be at or <b>after</b> the given line.
* </p>
*
* @param file
* @param line
* @return {@code -1} if no match is found.
*/
public int findClosestBreakpointLine(IPath file, int line) {
if (line < 0) {
return line;
}
Long fileId = fileIds.get(file);
if (fileId != null) {
NavigableSet<Integer> lineMap = lineMaps.get(fileId);
Integer bestMatch = lineMap == null ? null : lineMap.ceiling(line);
if (bestMatch != null) {
return bestMatch;
}
}
return -1;
}
public boolean requiresFullBuild() {
return projectRedefinable == null;
}
public ProjectRedefinable getBaseline() {
initProjectRedefinable();
return projectRedefinable;
}
}