./codecover/src/org/codecover/eclipse/utils/EclipseMASTLinkage.java

/******************************************************************************
 * Copyright (c) 2007 Stefan Franke, Robert Hanussek, Benjamin Keil,          *
 *                    Steffen Kieß, Johannes Langauf,                         *
 *                    Christoph Marian Müller, Igor Podolskiy,                *
 *                    Tilmann Scheller, Michael Starzmann, Markus Wittlinger  *
 * 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                                  *
 ******************************************************************************/

package org.codecover.eclipse.utils;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.codecover.eclipse.CodeCoverPlugin;
import org.codecover.model.MASTBuilder;
import org.codecover.model.TestSessionContainer;
import org.codecover.model.mast.HierarchyLevel;
import org.codecover.model.mast.Location;
import org.codecover.model.mast.SourceFile;
import org.codecover.model.utils.Logger;
import org.codecover.model.utils.file.FileTool;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.internal.core.SourceType;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.texteditor.ITextEditor;

/**
 * Utilities to provide mappings between Eclipse Models and the MAST.
 * 
 * @author Johannes Langauf
 * @version 1.0 ($Id: EclipseMASTLinkage.java 75 2011-06-28 13:28:30Z dobrowolsk $)
 */
public abstract class EclipseMASTLinkage {
    
    /* Note: Mapping between SourceFile and IFile is unavailable, because:
     * 1. source files don't have paths relative to root (could be added)
     * 2. instrumentation root is unknown (could be added)
     * 3. no Mapping between instrumentation root and project root is possible
     *   (no way, if TSC has no added information from Eclipse)
     * 
     * Currently the best approximation is to search the workspace for
     * equivalent FileName and Content.
     */
    
    /**
     * Select and show loc in an editor with coverage
     * highlighting.
     * 
     * @param editor
     * the editor showing the unchanged loc.getSourceFile()
     * @param loc
     * the position to show
     * 
     * @return true
     */
    public static boolean showInEditor(ITextEditor editor, Location loc) {
        editor.selectAndReveal(loc.getStartOffset(), loc.getLength());
        
        return true;
    }
    
    /**
     * Open a java editor that contains the code of the header of hLev.
     * 
     * @param hLev
     *            the hLev that is a top level class or contained in one
     * @param tsc
     *            the Java test session container that contains
     *            hLev
     * @return the java editor
     */
    public static ITextEditor openClassInEditor(HierarchyLevel hLev,
            TestSessionContainer tsc) {
        ITextEditor result = null;
        
        hLev = MAST.getTopLevelClass(hLev, tsc);
        
        /* find corresponding compilation unit by class name */
        String fqn = MAST.getFQName(hLev, tsc);
        Set cuSet; 
        cuSet = EclipseMASTLinkage.Eclipse.findCompilationUnit(fqn);
        
        /* got matches by qualified class name */
        
        /* filter for matches that fit class */
        Location target = MAST.getHighlightLocation(hLev);
        SourceFile source = target.getFile();
        Set unchangedCu = null;
        unchangedCu = new HashSet(cuSet.size());
        for (ICompilationUnit cu: cuSet) {
            /* check identity */
            IResource resource = cu.getResource();
            if (resource != null) {
                IFile file = (IFile)resource;
                if (equals(file, source)) {
                    unchangedCu.add(cu);
                }
            }
        }
        
        int matchCount = unchangedCu.size();
        if (matchCount > 0) {
            if (matchCount > 1) {
                CodeCoverPlugin.getDefault().getLogger().warning("found too many matching cu (using first):" + matchCount); //$NON-NLS-1$
            }
            result = Eclipse.openMember(unchangedCu.iterator().next());
        } else {
            CodeCoverPlugin.getDefault().getLogger().debug("found no matching cu for: " + fqn); //$NON-NLS-1$
        }
        
        return result;
    }

    /**
     * Check if the given resouce and sourceFile are identical. i.e.
     * instrumenting the resource with the current settings would produce the
     * same SourceFile as instrumenting sourceFile.
     *
     * @param file
     * a synchronized Eclipse resource
     * @param sourceFile
     * the original source file to match
     * 
     * @return true, iff resource matches all attributes in
     * sourceFile
     */
    @SuppressWarnings("nls")
    public static boolean equals(IFile file, SourceFile sourceFile) {
        if (! file.exists()) {
            throw new IllegalArgumentException("file does not exist");
        }
        
        /* file name (without path) */
        if (!sourceFile.getFileName().equals(file.getName())) {
            return false;
        }
        
        /* extract content of file */
        String contentOfFile = null;
        try {
            contentOfFile = Eclipse.getContent(file);
        } catch (Exception e) {
            //TODO: is there a better way to handle files that are not synchronized (happened) or not local
            CodeCoverPlugin.getDefault().getLogger().warning("possible false"
                    + " negative comparing '" + file.toString()
                    + "' to match '" + sourceFile.toString()
                    + "', because content is unavailable:", e);
            return false;
        }
        String sourceFileContent = sourceFile.getContent();
        if (! sourceFileContent.equals(contentOfFile)) {
        	return false;
        }
        
        return true;
    }

    /**
     * Check if the given resource and sourceFile are identical. i.e.
     * instrumenting the resource with the current settings would produce the
     * same as instrumenting sourceFile.
     * 
     * @param resource
     *            the Eclipse resource
     * @param sourceFile
     *            the original source file to match
     * @return true, iff resource matches all attributes in
     *         sourceFile
     */
    public static boolean equals(IResource resource, SourceFile sourceFile) {
        IFile file;
        if (resource instanceof IFile) {
            /* shortcut */
            file = (IFile) resource;
        } else {
            file = (IFile) resource.getAdapter(IFile.class);
            if (file == null) {
                throw new IllegalArgumentException("resource is no file"); //$NON-NLS-1$
            }
        }
        return equals(file, sourceFile);
    }
    
    /**
     * Create a Location that represents the current selection of an editor.
     * 
     * @param editor
     * @param file
     * the file that is opened in editor
     * @return
     * the current selection of editor
     */
    @SuppressWarnings("nls")
    public static Location getSelection(ITextEditor editor, SourceFile file) {
        MASTBuilder builder = new MASTBuilder(Logger.NULL);
        
        /* determine cursor position and selection */
        int offset = -1;
        int length = -1;        
        ISelection s = editor.getSelectionProvider().getSelection();
        ITextSelection selection;
        if (s instanceof ITextSelection) {
            selection = (ITextSelection) s;
            offset = selection.getOffset();
            length = selection.getLength();
        } else {
            throw new IllegalArgumentException("editor does not return a"
                    + " proper ITextSelection: " + s);
        }
        
        /* offset and length are set to the selection of editor */

        return builder.createLocation(file, offset, offset + length);        
    }
    

    /**
     * Find the corresponding CodeCover MAST HierarchyLevel to the given
     * Eclipse Java Element.
     * 
     * @param code
     *   root of code (MAST) to search
     * @param element
     *   search key
     * @return
     *   the HierarchyLevel of element, null if not found
     */
    public static HierarchyLevel findSource (HierarchyLevel code, IJavaElement element) {
        HierarchyLevel result = null; //null until element is found
        
        /* check input */
        if (code == null) {
            throw new IllegalArgumentException ("code is null"); //$NON-NLS-1$
        }
        if (element == null) {
            throw new IllegalArgumentException ("element is null"); //$NON-NLS-1$
        }
    
        /* get corresponding ICompilationUnit */
        ICompilationUnit compilationUnit;
        if (element.getElementType() == IJavaElement.COMPILATION_UNIT) {
            compilationUnit = (ICompilationUnit) element;
        } else {
            compilationUnit = (ICompilationUnit)
            element.getAncestor(IJavaElement.COMPILATION_UNIT);
        }
    
        if (compilationUnit != null) {
    
            /* Extract fully qualified class name with its package */
            String fileName = compilationUnit.getElementName();
            String className = fileName.split("\\.")[0]; //$NON-NLS-1$
            IPackageFragment pkgF = (IPackageFragment) compilationUnit.getAncestor(IJavaElement.PACKAGE_FRAGMENT);
            if (pkgF != null) {
                String path[];
                if (pkgF.getElementName().equals("")) { //$NON-NLS-1$
                    path = new String[] { className };
                } else {
                    className = pkgF.getElementName() + "." + className; //$NON-NLS-1$
                    path = className.split("\\."); //$NON-NLS-1$
                }
    
                /* find HierarchyLevel for the class by name */
                HierarchyLevel current = code;
                boolean found = true;
    
                for (int i = 0; i < path.length && found; ++i) {    
                    found = false;
                    
                    /* find next HierarchyLevel in path */
                    for (HierarchyLevel l: current.getChildren()) {
                        if (l.getName().equals(path[i])) {
                            current = l;
                            found = true;
                            break;
                        }
                    }
                }
                if (found) {
    
                    /* the whole path was successfully traversed */
                    result = current;
                }
            }
        }
    
        return result;
    }


    /**
     * Tools to work with MAST.
     * 
     * @version 1.0 ($Id: EclipseMASTLinkage.java 75 2011-06-28 13:28:30Z dobrowolsk $)
     * @author Johannes Langauf
     */
    public static class MAST {
        
        /**
         * Get a Location representing hLev in the
         * code. Does not have to contain all code of hLev.
         * 
         * @param hLev
         * a HierarchyLevel with a none empty header
         * 
         * @return a Location to represent hLev
         * @see HierarchyLevel#getHeader()
         */
        public static Location getHighlightLocation(HierarchyLevel hLev) {    
            List locList = hLev.getHeader().getLocations();
            Location target;
            if (locList.size() > 0) {
                target = locList.get(0);
            } else {
                throw new IllegalArgumentException("hLev has no header Location."); //$NON-NLS-1$
            }

            return target; 
        }
        
        /**
         * Get the top level class of a java hierarchy level as defined in the
         * JLS:
         * http://java.sun.com/docs/books/jls/third_edition/html/classes.html#246201
         * 
         * @param hLev
         * a top level class or its descendant
         * @param tsc
         * the java test session container that contains hLev
         * @return
         * the top level class that contains hLev or hLev, if it's the top level
         * class
         */
        public static HierarchyLevel getTopLevelClass(HierarchyLevel hLev,
                TestSessionContainer tsc) {
            HierarchyLevel parentPackage = hLev;
            HierarchyLevel topLevelClass = null;
            
            while (! parentPackage.getType().getInternalName().equals("package") //$NON-NLS-1$
                    && ! parentPackage.getType().getInternalName().equals("default package")) { //$NON-NLS-1$
                topLevelClass = parentPackage; 
                parentPackage = tsc.getParentOfHierarchyLevel(parentPackage);
            }
            
            return topLevelClass;
        }
        
        /**
         * Get the package of hLev.
         * 
         * @param hLev
         * a class or package
         * @param tsc
         * the java test session container that contains hLev
         * @return
         * the package of hLev, hLev itself if it's a package
         */
        public static HierarchyLevel getPackage(HierarchyLevel hLev,
                TestSessionContainer tsc) {
            //same as above, just return the parentPackage
            HierarchyLevel parentPackage = hLev;
            @SuppressWarnings("unused")
            HierarchyLevel topLevelClass = null;
            
            while (! parentPackage.getType().getInternalName().equals("package") //$NON-NLS-1$
                    && ! parentPackage.getType().getInternalName().equals("default package")) { //$NON-NLS-1$
                topLevelClass = parentPackage; 
                parentPackage = tsc.getParentOfHierarchyLevel(parentPackage);
            }
            
            return parentPackage;
        }
        
        /**
         * Return the fully qualified name of a compilation unit in a Java TSC.
         * Types of Hierarchy Levels can be found in
         * HierarchyLevelTypeProvider.
         * 
         * @param element
         * the compilation unit, valid types are class, interface, enum, &at;interface,  
         * @param tsc
         * the test session container containing element
         * 
         * @return the fully qualified name of element
         * @see org.codecover.instrumentation.java15.HierarchyLevelTypeProvider
         */
        @SuppressWarnings("nls")
        public static String getFQName(HierarchyLevel element,
                                       TestSessionContainer tsc) {
            if (element == null) {
                throw new IllegalArgumentException("HierarchyLevel is null");
            }
            String internalName =  element.getType().getInternalName();
            if (!internalName.equals("class")
                    && !internalName.equals("interface")
                    && !internalName.equals("enum")
                    && !internalName.equals("@interface")) {
                throw new IllegalArgumentException(
                        "HierarchyLevel is no Compilation unit. Type: "
                        + internalName);
            }
            if (tsc == null) {
                throw new IllegalArgumentException("tsc is null");
            }
            
            /* extract name of innermost level */
            String fqName = element.getName();
            element = tsc.getParentOfHierarchyLevel(element);
            
            /* add names of parent levels, not including the unnamed top level */
            while (!element.getType().getInternalName().equals("default package")) {
                fqName = element.getName() + "." + fqName;
                element = tsc.getParentOfHierarchyLevel(element);
            }
            return fqName;
        }
    }
    
    /**
     * Tools to work with Eclipse Java model.
     * 
     * @version 1.0 ($Id: EclipseMASTLinkage.java 75 2011-06-28 13:28:30Z dobrowolsk $)
     * @author Johanne Langauf
     */
    public static class Eclipse {
        
        /**
         * Read a whole file into a String.
         * 
         * @param file
         * the synchronized, existing, readable and local file
         * 
         * @return
         * the contents of the file
         * 
         * @see IResource#isSynchronized(int)
         * @see IResource#isLocal(int)
         */
        public static String getContent(IFile file) {
            String contentOfFile = null;
            try {
                InputStream inStream = file.getContents();
                Charset charsetOfFile = Charset.forName(file.getCharset());
                contentOfFile = FileTool.getContentFromStream(inStream, charsetOfFile);
            } catch (CoreException e) {
                boolean isSynchronized = false;
                boolean isLocal = false;
                try {
                    isSynchronized = file.isSynchronized(IResource.DEPTH_ZERO);
                    isLocal = file.isLocal(IResource.DEPTH_ZERO);
                } catch (Exception ex) {
                    CodeCoverPlugin.getDefault().getLogger().fatal(
                            "something is totally broken", ex); //$NON-NLS-1$
                }
                if (! isSynchronized) {
                    // There seems to be no way to handle this without
                    // bothering the user. Like the editor does. Asking
                    // the user if he want's to synchronize a file he hasn'
                    // t even seen like the editor does is stupid so we let
                    // callers take care of what they want.
                    throw new IllegalArgumentException("file is not in sync"); //$NON-NLS-1$
                }
                if (! isLocal) {
                    //TODO: Can this happen? - if so handle gracefully.
                    CodeCoverPlugin.getDefault().getLogger().error(
                            "Unexpected not local resource.  Please " //$NON-NLS-1$
                            + "file a bug with the whole message.", e); //$NON-NLS-1$
                    throw new IllegalArgumentException("file is not local"); //$NON-NLS-1$
                }

            } catch (IOException e) {
                CodeCoverPlugin.getDefault().getLogger().debug("Can't read: " //$NON-NLS-1$
                        + file.getFullPath().toOSString(), e);
                throw new IllegalArgumentException("File not readable."); //$NON-NLS-1$
            }
            return contentOfFile;
        }
        
        /**
         * Find a type by its name.
         * 
         * @param fQName
         * fully qualified name of the type
         * @return
         * list of matching compilation units
         */
        public static Set findCompilationUnit(String fQName) {
           final Set result = new HashSet(1);
           
           SearchPattern pattern = SearchPattern.createPattern(fQName,
                   IJavaSearchConstants.TYPE,
                   IJavaSearchConstants.DECLARATIONS,
                   SearchPattern.R_CASE_SENSITIVE
                   | SearchPattern.R_EXACT_MATCH);

           IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
           //to narrow search, e.g. on one project you could use this:
//           IJavaProject p = null;
//           IJavaElement project[] = {p};
//           IJavaSearchScope scope;
//           scope = SearchEngine.createJavaSearchScope(project);

           SearchRequestor requestor = new SearchRequestor() {
               @Override
               public void acceptSearchMatch(SearchMatch match) throws CoreException {
                   if (match.getAccuracy() == SearchMatch.A_ACCURATE) {
                       addMatch(match.getResource());
                       addMatch(match.getElement());                   
                   }
               }
               private void addMatch(Object match) {
                   ICompilationUnit cu = null;
                   if (match instanceof SourceType) {
                       SourceType sf = 
                           (SourceType)match;
                       cu = sf.getCompilationUnit();
                   }
                   if (cu == null && match instanceof IJavaElement) {
                       
                       IJavaElement adaptable = (IJavaElement)match;
                       
                       cu = (ICompilationUnit) adaptable.getAdapter(ICompilationUnit.class);
                   }
                   if (cu != null) {
                       synchronized (result) {
                           result.add(cu);
                       }
                   }
               }
           };
           SearchEngine searchEngine = new SearchEngine();
           
           try {
               searchEngine.search(pattern,
                       new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()},
                       scope, requestor, null);
           } catch (CoreException e) {
               CodeCoverPlugin.getDefault().getLogger().warning(
                       "Ignoring failed search for: " + fQName, e); //$NON-NLS-1$
           }

           return result;
        }
        
        /**
         * Open a Java-Element in an ITextEditor. Don't focus it.
         * 
         * @param member
         * the element to open
         * 
         * @return the editor showing member, null if member can't
         *  be opened
         */
        public static ITextEditor openMember(IMember member) {
            ICompilationUnit cu = member.getCompilationUnit();
            return openMember(cu);
        }
        
        /**
         * Open a Java-Element in an ITextEditor. Don't focus it.
         * 
         * @param cu
         * the element to open
         * 
         * @return the editor showing cu, null if member can't
         *  be opened
         */
        public static ITextEditor openMember(ICompilationUnit cu) {
            ITextEditor editor;
            try {
                IEditorPart ed = JavaUI.openInEditor(cu, false, false);
                editor = (ITextEditor) ed.getAdapter(ITextEditor.class);
            } catch (PartInitException e) {
                /* the editor could not be initialized or no workbench page is
                 * active */
                return null;
            } catch (JavaModelException e) {
                /* cu does not exist or an exception occurs while accessing its
                 * underlying resource */
                return null;
            }
            
            return editor;
        }
    }
}