| /******************************************************************************* |
| * Copyright (c) 2000, 2005 IBM Corporation and others. |
| * 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: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.jdt.internal.ui.compare; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.util.*; |
| |
| import org.eclipse.jface.text.*; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| |
| import org.eclipse.jdt.core.*; |
| import org.eclipse.jdt.core.compiler.*; |
| import org.eclipse.jdt.core.dom.AST; |
| import org.eclipse.jdt.core.dom.ASTParser; |
| import org.eclipse.jdt.core.dom.CompilationUnit; |
| import org.eclipse.jdt.internal.ui.JavaPlugin; |
| |
| import org.eclipse.compare.*; |
| import org.eclipse.compare.IResourceProvider; |
| import org.eclipse.compare.structuremergeviewer.*; |
| |
| |
| public class JavaStructureCreator implements IStructureCreator { |
| |
| private Map fDefaultCompilerOptions; |
| |
| /** |
| * RewriteInfos are used temporarily when rewriting the diff tree |
| * in order to combine similar diff nodes ("smart folding"). |
| */ |
| static class RewriteInfo { |
| |
| boolean fIsOut= false; |
| |
| JavaNode fAncestor= null; |
| JavaNode fLeft= null; |
| JavaNode fRight= null; |
| |
| ArrayList fChildren= new ArrayList(); |
| |
| void add(IDiffElement diff) { |
| fChildren.add(diff); |
| } |
| |
| void setDiff(ICompareInput diff) { |
| if (fIsOut) |
| return; |
| |
| fIsOut= true; |
| |
| JavaNode a= (JavaNode) diff.getAncestor(); |
| JavaNode y= (JavaNode) diff.getLeft(); |
| JavaNode m= (JavaNode) diff.getRight(); |
| |
| if (a != null) { |
| if (fAncestor != null) |
| return; |
| fAncestor= a; |
| } |
| if (y != null) { |
| if (fLeft != null) |
| return; |
| fLeft= y; |
| } |
| if (m != null) { |
| if (fRight != null) |
| return; |
| fRight= m; |
| } |
| |
| fIsOut= false; |
| } |
| |
| /** |
| * Returns true if some nodes could be successfully combined into one. |
| */ |
| boolean matches() { |
| return !fIsOut && fAncestor != null && fLeft != null && fRight != null; |
| } |
| } |
| |
| public JavaStructureCreator() { |
| } |
| |
| void setDefaultCompilerOptions(Map compilerSettings) { |
| fDefaultCompilerOptions= compilerSettings; |
| } |
| |
| /** |
| * Returns the name that appears in the enclosing pane title bar. |
| */ |
| public String getName() { |
| return CompareMessages.JavaStructureViewer_title; |
| } |
| |
| /** |
| * Returns a tree of JavaNodes for the given input |
| * which must implement the IStreamContentAccessor interface. |
| * In case of error null is returned. |
| */ |
| public IStructureComparator getStructure(final Object input) { |
| String contents= null; |
| char[] buffer= null; |
| IDocument doc= CompareUI.getDocument(input); |
| if (doc == null) { |
| if (input instanceof IStreamContentAccessor) { |
| IStreamContentAccessor sca= (IStreamContentAccessor) input; |
| try { |
| contents= JavaCompareUtilities.readString(sca); |
| } catch (CoreException ex) { |
| // return null indicates the error. |
| return null; |
| } |
| } |
| |
| if (contents != null) { |
| int n= contents.length(); |
| buffer= new char[n]; |
| contents.getChars(0, n, buffer, 0); |
| |
| doc= new Document(contents); |
| //CompareUI.registerDocument(input, doc); |
| JavaCompareUtilities.setupDocument(doc); |
| } |
| } |
| |
| Map compilerOptions= null; |
| if (input instanceof IResourceProvider) { |
| IResource resource= ((IResourceProvider) input).getResource(); |
| if (resource != null) { |
| IJavaElement element= JavaCore.create(resource); |
| if (element != null) { |
| IJavaProject javaProject= element.getJavaProject(); |
| if (javaProject != null) |
| compilerOptions= javaProject.getOptions(true); |
| } |
| } |
| } |
| if (compilerOptions == null) |
| compilerOptions= fDefaultCompilerOptions; |
| |
| if (doc != null) { |
| boolean isEditable= false; |
| if (input instanceof IEditableContent) |
| isEditable= ((IEditableContent) input).isEditable(); |
| |
| // we hook into the root node to intercept all node changes |
| JavaNode root= new JavaNode(doc, isEditable) { |
| void nodeChanged(JavaNode node) { |
| save(this, input); |
| } |
| }; |
| |
| if (buffer == null) { |
| contents= doc.get(); |
| int n= contents.length(); |
| buffer= new char[n]; |
| contents.getChars(0, n, buffer, 0); |
| } |
| |
| ASTParser parser= ASTParser.newParser(AST.JLS3); |
| if (compilerOptions != null) |
| parser.setCompilerOptions(compilerOptions); |
| parser.setSource(buffer); |
| parser.setFocalPosition(0); |
| CompilationUnit cu= (CompilationUnit) parser.createAST(null); |
| cu.accept(new JavaParseTreeBuilder(root, buffer, true)); |
| |
| return root; |
| } |
| return null; |
| } |
| |
| /** |
| * Returns true because this IStructureCreator knows how to save. |
| */ |
| public boolean canSave() { |
| return true; |
| } |
| |
| public void save(IStructureComparator node, Object input) { |
| if (node instanceof JavaNode && input instanceof IEditableContent) { |
| IDocument document= ((JavaNode)node).getDocument(); |
| IEditableContent bca= (IEditableContent) input; |
| String contents= document.get(); |
| String encoding= null; |
| if (input instanceof IEncodedStreamContentAccessor) { |
| try { |
| encoding= ((IEncodedStreamContentAccessor)input).getCharset(); |
| } catch (CoreException e1) { |
| // ignore |
| } |
| } |
| if (encoding == null) |
| encoding= ResourcesPlugin.getEncoding(); |
| byte[] bytes; |
| try { |
| bytes= contents.getBytes(encoding); |
| } catch (UnsupportedEncodingException e) { |
| bytes= contents.getBytes(); |
| } |
| bca.setContent(bytes); |
| } |
| } |
| |
| /** |
| * Returns the contents of the given node as a string. |
| * This string is used to test the content of a Java element |
| * for equality. Is is never shown in the UI, so any string representing |
| * the content will do. |
| * @param node must implement the IStreamContentAccessor interface |
| * @param ignoreWhiteSpace if true all Java white space (incl. comments) is removed from the contents. |
| */ |
| public String getContents(Object node, boolean ignoreWhiteSpace) { |
| |
| if (! (node instanceof IStreamContentAccessor)) |
| return null; |
| |
| IStreamContentAccessor sca= (IStreamContentAccessor) node; |
| String content= null; |
| try { |
| content= JavaCompareUtilities.readString(sca); |
| } catch (CoreException ex) { |
| JavaPlugin.log(ex); |
| return null; |
| } |
| |
| if (ignoreWhiteSpace) { // we return everything but Java whitespace |
| |
| // replace comments and whitespace by a single blank |
| StringBuffer buf= new StringBuffer(); |
| char[] b= content.toCharArray(); |
| |
| // to avoid the trouble when dealing with Unicode |
| // we use the Java scanner to extract non-whitespace and non-comment tokens |
| IScanner scanner= ToolFactory.createScanner(true, true, false, false); // however we request Whitespace and Comments |
| scanner.setSource(b); |
| try { |
| int token; |
| while ((token= scanner.getNextToken()) != ITerminalSymbols.TokenNameEOF) { |
| switch (token) { |
| case ITerminalSymbols.TokenNameWHITESPACE: |
| case ITerminalSymbols.TokenNameCOMMENT_BLOCK: |
| case ITerminalSymbols.TokenNameCOMMENT_JAVADOC: |
| case ITerminalSymbols.TokenNameCOMMENT_LINE: |
| int l= buf.length(); |
| if (l > 0 && buf.charAt(l-1) != ' ') |
| buf.append(' '); |
| break; |
| default: |
| buf.append(scanner.getCurrentTokenSource()); |
| buf.append(' '); |
| break; |
| } |
| } |
| content= buf.toString(); // success! |
| } catch (InvalidInputException ex) { |
| // NeedWork |
| } |
| } |
| return content; |
| } |
| |
| /** |
| * Returns true since this IStructureCreator can rewrite the diff tree |
| * in order to fold certain combinations of additons and deletions. |
| */ |
| public boolean canRewriteTree() { |
| return true; |
| } |
| |
| /** |
| * Tries to detect certain combinations of additons and deletions |
| * as renames or signature changes and foldes them into a single node. |
| */ |
| public void rewriteTree(Differencer differencer, IDiffContainer root) { |
| |
| HashMap map= new HashMap(10); |
| |
| Object[] children= root.getChildren(); |
| for (int i= 0; i < children.length; i++) { |
| DiffNode diff= (DiffNode) children[i]; |
| JavaNode jn= (JavaNode) diff.getId(); |
| |
| if (jn == null) |
| continue; |
| int type= jn.getTypeCode(); |
| |
| // we can only combine methods or constructors |
| if (type == JavaNode.METHOD || type == JavaNode.CONSTRUCTOR) { |
| |
| // find or create a RewriteInfo for all methods with the same name |
| String name= jn.extractMethodName(); |
| RewriteInfo nameInfo= (RewriteInfo) map.get(name); |
| if (nameInfo == null) { |
| nameInfo= new RewriteInfo(); |
| map.put(name, nameInfo); |
| } |
| nameInfo.add(diff); |
| |
| // find or create a RewriteInfo for all methods with the same |
| // (non-empty) argument list |
| String argList= jn.extractArgumentList(); |
| RewriteInfo argInfo= null; |
| if (argList != null && !argList.equals("()")) { //$NON-NLS-1$ |
| argInfo= (RewriteInfo) map.get(argList); |
| if (argInfo == null) { |
| argInfo= new RewriteInfo(); |
| map.put(argList, argInfo); |
| } |
| argInfo.add(diff); |
| } |
| |
| switch (diff.getKind() & Differencer.CHANGE_TYPE_MASK) { |
| case Differencer.ADDITION: |
| case Differencer.DELETION: |
| // we only consider addition and deletions |
| // since a rename or arg list change looks |
| // like a pair of addition and deletions |
| if (type != JavaNode.CONSTRUCTOR) |
| nameInfo.setDiff(diff); |
| |
| if (argInfo != null) |
| argInfo.setDiff(diff); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| // recurse |
| rewriteTree(differencer, diff); |
| } |
| |
| // now we have to rebuild the diff tree according to the combined |
| // changes |
| Iterator it= map.keySet().iterator(); |
| while (it.hasNext()) { |
| String name= (String) it.next(); |
| RewriteInfo i= (RewriteInfo) map.get(name); |
| if (i.matches()) { // we found a RewriteInfo that could be succesfully combined |
| |
| // we have to find the differences of the newly combined node |
| // (because in the first pass we only got a deletion and an addition) |
| DiffNode d= (DiffNode) differencer.findDifferences(true, null, root, i.fAncestor, i.fLeft, i.fRight); |
| if (d != null) {// there better should be a difference |
| d.setDontExpand(true); |
| Iterator it2= i.fChildren.iterator(); |
| while (it2.hasNext()) { |
| IDiffElement rd= (IDiffElement) it2.next(); |
| root.removeToRoot(rd); |
| d.add(rd); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * If selector is an IJavaElement this method tries to return an |
| * IStructureComparator object for it. |
| * In case of error or if the given selector cannot be found |
| * null is returned. |
| * @param selector the IJavaElement to extract |
| * @param input must implement the IStreamContentAccessor interface. |
| */ |
| public IStructureComparator locate(Object selector, Object input) { |
| |
| if (!(selector instanceof IJavaElement)) |
| return null; |
| |
| // try to build the JavaNode tree from input |
| IStructureComparator structure= getStructure(input); |
| if (structure == null) // we couldn't parse the structure |
| return null; // so we can't find anything |
| |
| // build a path |
| String[] path= createPath((IJavaElement) selector); |
| |
| // find the path in the JavaNode tree |
| return find(structure, path, 0); |
| } |
| |
| 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= JavaCompareUtilities.getJavaElementID(je); |
| if (name == null) |
| return null; |
| args.add(name); |
| if (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; |
| } |
| |
| /** |
| * Recursivly extracts the given path from the tree. |
| */ |
| private static IStructureComparator find(IStructureComparator tree, String[] path, int index) { |
| if (tree != null) { |
| Object[] children= tree.getChildren(); |
| if (children != null) { |
| for (int i= 0; i < children.length; i++) { |
| IStructureComparator child= (IStructureComparator) children[i]; |
| if (child instanceof ITypedElement && child instanceof DocumentRangeNode) { |
| String n1= null; |
| if (child instanceof DocumentRangeNode) |
| n1= ((DocumentRangeNode)child).getId(); |
| if (n1 == null) |
| n1= ((ITypedElement)child).getName(); |
| String n2= path[index]; |
| if (n1.equals(n2)) { |
| if (index == path.length-1) |
| return child; |
| IStructureComparator result= find(child, path, index+1); |
| if (result != null) |
| return result; |
| } |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns true if the given IJavaElement maps to a JavaNode. |
| * The JavaHistoryAction uses this function to determine whether |
| * a selected Java element can be replaced by some piece of |
| * code from the local history. |
| */ |
| static boolean hasEdition(IJavaElement je) { |
| |
| if (je instanceof IMember && ((IMember)je).isBinary()) |
| return false; |
| |
| switch (je.getElementType()) { |
| case IJavaElement.COMPILATION_UNIT: |
| case IJavaElement.TYPE: |
| case IJavaElement.FIELD: |
| case IJavaElement.METHOD: |
| case IJavaElement.INITIALIZER: |
| case IJavaElement.PACKAGE_DECLARATION: |
| case IJavaElement.IMPORT_CONTAINER: |
| case IJavaElement.IMPORT_DECLARATION: |
| return true; |
| } |
| return false; |
| } |
| } |