/* | |
* (c) Copyright IBM Corp. 2000, 2001. | |
* All Rights Reserved. | |
*/ | |
package org.eclipse.jdt.internal.debug.core.hcr; | |
import java.io.*; | |
import java.util.*; | |
import org.eclipse.core.resources.*; | |
import org.eclipse.core.runtime.CoreException; | |
import org.eclipse.jdt.core.*; | |
import org.eclipse.jdt.internal.compiler.*; | |
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; | |
import org.eclipse.jdt.internal.core.JavaElement; | |
/** | |
* A <code>CompilationUnitDelta</code> represents the source code changes between | |
* a CU in the workspace and the same CU at some point in the past | |
* (from the local history). | |
* <p> | |
* This functionality is used in the context of Hot Code Replace | |
* to determine which stack frames are affected (and need to be dropped) | |
* by a class reload in the Java VM. | |
* <p> | |
* Typically a <code>CompilationUnitDelta</code> object is generated for a CU | |
* when the associated class is replaced in the VM. | |
* The <code>CompilationUnitDelta</code> calculates | |
* the differences between the current version of the CU and the version in the | |
* local history that was effective at the given <bold>last</bold> build time. | |
* The differences are stored as a tree which allows for an efficient implementation | |
* of a <code>hasChanged(IMember)</code> method. | |
*/ | |
public class CompilationUnitDelta { | |
private static final boolean DEBUG= false; | |
private static class SimpleJavaElement { | |
private String fName; | |
private HashMap fChildren; | |
// private int fChangeType; | |
SimpleJavaElement(SimpleJavaElement parent, int changeType, String name) { | |
fName= name; | |
// fChangeType= changeType; | |
if (parent != null) { | |
if (parent.fChildren == null) | |
parent.fChildren= new HashMap(); | |
parent.fChildren.put(name, this); | |
} | |
} | |
void dump(int level) { | |
for (int i= 0; i < level; i++) | |
System.out.print(" "); //$NON-NLS-1$ | |
System.out.println(fName); | |
if (fChildren != null) { | |
Iterator iter= fChildren.values().iterator(); | |
while (iter.hasNext()) { | |
SimpleJavaElement e= (SimpleJavaElement) iter.next(); | |
e.dump(level+1); | |
} | |
} | |
} | |
boolean find(String[] path, int start) { | |
if (start >= path.length) | |
return true; | |
String key= path[start]; | |
if (fChildren != null) { | |
SimpleJavaElement child= (SimpleJavaElement) fChildren.get(key); | |
if (child != null) | |
return child.find(path, start+1); | |
} | |
return false; | |
} | |
} | |
/** | |
* Used to bail out from ProblemFactory. | |
*/ | |
private static class ParseError extends Error { | |
} | |
/** | |
* This problem factory aborts parsing on first error. | |
*/ | |
private static class ProblemFactory implements IProblemFactory { | |
public IProblem createProblem(char[] originatingFileName, int problemId, String[] arguments, int severity, int startPosition, int endPosition, int lineNumber) { | |
throw new ParseError(); | |
} | |
public Locale getLocale() { | |
return Locale.getDefault(); | |
} | |
public String getLocalizedMessage(int problemId, String[] problemArguments) { | |
return "" + problemId; //$NON-NLS-1$ | |
} | |
} | |
private ICompilationUnit fCompilationUnit; | |
private SimpleJavaElement fRoot; | |
private boolean fHasHistory= false; | |
/** | |
* Creates a new <code>CompilationUnitDelta object that calculates and stores | |
* the changes of the given CU since some point in time. | |
*/ | |
public CompilationUnitDelta(ICompilationUnit cu, long timestamp) throws CoreException { | |
if (cu.isWorkingCopy()) | |
cu= (ICompilationUnit) cu.getOriginalElement(); | |
fCompilationUnit= cu; | |
// find underlying file | |
IFile file= (IFile) cu.getUnderlyingResource(); | |
// get available editions | |
IFileState[] states= file.getHistory(null); | |
if (states == null || states.length <= 0) | |
return; | |
fHasHistory= true; | |
IFileState found= null; | |
// find edition just before the given time stamp | |
for (int i= 0; i < states.length; i++) { | |
IFileState state= states[i]; | |
long d= state.getModificationTime(); | |
if (d < timestamp) { | |
found= state; | |
break; | |
} | |
} | |
if (found == null) | |
found= states[states.length-1]; | |
InputStream oldContents= null; | |
InputStream newContents= null; | |
try { | |
oldContents= found.getContents(); | |
newContents= file.getContents(); | |
} catch (CoreException ex) { | |
return; | |
} | |
JavaNode oldStructure= parse(oldContents); | |
JavaNode newStructure= parse(newContents); | |
final boolean[] memberDeleted= new boolean[1]; // visitor returns result here | |
Differencer differencer= new Differencer() { | |
protected Object visit(Object data, int result, Object ancestor, Object left, Object right) { | |
String name= null; | |
switch (result) { | |
case Differencer.CHANGE: | |
name= ((JavaNode)left).getId(); | |
break; | |
case Differencer.ADDITION: | |
name= ((JavaNode)right).getId(); | |
break; | |
case Differencer.DELETION: | |
name= ((JavaNode)left).getId(); | |
memberDeleted[0]= true; | |
break; | |
default: | |
break; | |
} | |
if (name != null) | |
return new SimpleJavaElement((SimpleJavaElement) data, result, name); | |
return null; | |
} | |
protected boolean contentsEqual(Object o1, Object o2) { | |
String s1= ((JavaNode) o1).getContents(); | |
String s2= ((JavaNode) o2).getContents(); | |
return s1.equals(s2); | |
} | |
protected Object[] getChildren(Object input) { | |
if (input instanceof JavaNode) | |
return ((JavaNode)input).getChildren(); | |
return null; | |
} | |
}; | |
fRoot= (SimpleJavaElement) differencer.findDifferences(false, null, null, null, oldStructure, newStructure); | |
fHasHistory= (fRoot != null); // if no changes pretend that we had no history | |
if (DEBUG) { | |
if (fRoot != null) | |
fRoot.dump(0); | |
} | |
if (memberDeleted[0]) // shape change because of deleted members | |
fRoot= null; // throw diffs away since hasChanged(..) must always return true | |
} | |
/** | |
* Returns <code>true</code> | |
* <ul> | |
* <li>if the source of the given member has been changed, or | |
* <li>if the element has been deleted, or | |
* <li>if the element has been newly created | |
* </ul> | |
* after the initial timestamp. | |
* | |
* @exception AssertionFailedException if member is null or member is not a member of this CU. | |
*/ | |
public boolean hasChanged(IMember member) { | |
//Assert.isNotNull(member); | |
ICompilationUnit cu= member.getCompilationUnit(); | |
if (cu.isWorkingCopy()) { | |
cu= (ICompilationUnit) cu.getOriginalElement(); | |
} | |
//Assert.isTrue(cu.equals(fCompilationUnit)); | |
if (fRoot == null) { | |
if (fHasHistory) { | |
return true; // pessimistic: we have a history but we couldn't use it for some reason | |
} | |
return false; // optimistic: we have no history, so assume that member hasn't changed | |
} | |
String[] path= createPath(member); | |
if (DEBUG) { | |
for (int i= 0; i < path.length; i++) { | |
System.out.print(path[i] + ' '); | |
} | |
System.out.println(); | |
} | |
return fRoot.find(path, 0); | |
} | |
//---- private stuff ---------------------------------------------------------------- | |
/** | |
* Parses the given input stream and returns a tree of JavaNodes | |
* or a null in case of failure. | |
*/ | |
private static JavaNode parse(InputStream input) { | |
char[] buffer= readString(input); | |
if (buffer != null) { | |
JavaNode root= new JavaNode(buffer); | |
JavaParseTreeBuilder builder= new JavaParseTreeBuilder(root, buffer); | |
SourceElementParser parser= new SourceElementParser(builder, new ProblemFactory(), new CompilerOptions()); | |
try { | |
parser.parseCompilationUnit(builder, false); | |
} catch (ParseError ex) { | |
// parse error: bail out | |
return null; | |
} | |
return root; | |
} | |
return null; | |
} | |
private static String[] createPath(IJavaElement je) { | |
// build a path starting at the given Java element and walk | |
// up the parent chain until we reach a IWorkingCopy or ICompilationUnit | |
List args= new ArrayList(); | |
while (je != null) { | |
// each path component has a name that uses the same | |
// conventions as a JavaNode name | |
String name= getJavaElementID(je); | |
if (name == null) | |
return null; | |
args.add(name); | |
if (je instanceof IWorkingCopy || je instanceof ICompilationUnit) | |
break; | |
je= je.getParent(); | |
} | |
// revert the path | |
int n= args.size(); | |
String[] path= new String[n]; | |
for (int i= 0; i < n; i++) | |
path[i]= (String) args.get(n-1-i); | |
return path; | |
} | |
/** | |
* Returns a name for the given Java element that uses the same conventions | |
* as the JavaNode name of a corresponding element. | |
*/ | |
private static String getJavaElementID(IJavaElement je) { | |
if (je instanceof IMember && ((IMember)je).isBinary()) | |
return null; | |
StringBuffer sb= new StringBuffer(); | |
switch (je.getElementType()) { | |
case JavaElement.COMPILATION_UNIT: | |
sb.append(JavaElement.JEM_COMPILATIONUNIT); | |
break; | |
case JavaElement.TYPE: | |
sb.append(JavaElement.JEM_TYPE); | |
sb.append(je.getElementName()); | |
break; | |
case JavaElement.FIELD: | |
sb.append(JavaElement.JEM_FIELD); | |
sb.append(je.getElementName()); | |
break; | |
case JavaElement.METHOD: | |
sb.append(JavaElement.JEM_METHOD); | |
IMethod method= (IMethod) je; | |
sb.append(method.getElementName()); | |
// parameters | |
sb.append('('); | |
String[] types= method.getParameterTypes(); | |
int nParams= types != null ? types.length : 0; | |
for (int i= 0; i < nParams; i++) { | |
if (i > 0) | |
sb.append(", "); //$NON-NLS-1$ | |
if (types != null) | |
sb.append(unqualifyName(Signature.getSimpleName(Signature.toString(types[i])))); | |
} | |
sb.append(')'); | |
break; | |
case JavaElement.INITIALIZER: | |
String id= je.getHandleIdentifier(); | |
int pos= id.lastIndexOf(JavaElement.JEM_INITIALIZER); | |
if (pos >= 0) | |
sb.append(id.substring(pos)); | |
break; | |
case JavaElement.PACKAGE_DECLARATION: | |
sb.append(JavaElement.JEM_PACKAGEDECLARATION); | |
break; | |
case JavaElement.IMPORT_CONTAINER: | |
sb.append('<'); | |
break; | |
case JavaElement.IMPORT_DECLARATION: | |
sb.append(JavaElement.JEM_IMPORTDECLARATION); | |
sb.append(je.getElementName()); | |
break; | |
default: | |
return null; | |
} | |
return sb.toString(); | |
} | |
private static String unqualifyName(String qualifiedName) { | |
int index= qualifiedName.lastIndexOf('/'); | |
if (index > -1) { | |
return qualifiedName.substring(index + 1); | |
} | |
return qualifiedName; | |
} | |
/** | |
* Returns null if an error occurred. | |
*/ | |
private static char[] readString(InputStream is) { | |
if (is == null) | |
return null; | |
BufferedReader reader= null; | |
try { | |
StringBuffer buffer= new StringBuffer(); | |
char[] part= new char[2048]; | |
int read= 0; | |
reader= new BufferedReader(new InputStreamReader(is)); | |
while ((read= reader.read(part)) != -1) | |
buffer.append(part, 0, read); | |
char[] b= new char[buffer.length()]; | |
buffer.getChars(0, b.length, b, 0); | |
return b; | |
} catch (IOException ex) { | |
} finally { | |
if (reader != null) { | |
try { | |
reader.close(); | |
} catch (IOException ex) { | |
} | |
} | |
} | |
return null; | |
} | |
} |