/* * Contributions to FindBugs * Copyright (C) 2009, Andrei Loskutov * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package de.tobject.findbugs.builder; import java.io.File; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.regex.Pattern; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import de.tobject.findbugs.FindbugsPlugin; import de.tobject.findbugs.reporter.MarkerUtil; import de.tobject.findbugs.util.Util; import edu.umd.cs.findbugs.Project; import edu.umd.cs.findbugs.util.Archive; /** * An item to work on for FB analysis - this can be entire project or single * file or a java element without corresponding resource (like external library) * * @author Andrei */ public class WorkItem { private final IJavaElement javaElt; private final IResource resource; private final IProject project; public WorkItem(IJavaElement javaElt) { this(null, javaElt, javaElt.getJavaProject().getProject()); } public WorkItem(IResource resource) { this(resource, null, resource.getProject()); } private WorkItem(IResource resource, IJavaElement javaElt, IProject project) { this.resource = resource; this.javaElt = javaElt; this.project = project; Assert.isLegal(resource != null || javaElt != null); } public void addFilesToProject(Project fbProject, Map<IPath, IPath> outputLocations) { IResource res = getCorespondingResource(); if (res instanceof IProject) { for (IPath outDir : outputLocations.values()) { fbProject.addFile(outDir.toOSString()); } } else if (res instanceof IFolder) { // assumption: this is a source folder. boolean added = addClassesForFolder((IFolder) res, outputLocations, fbProject); if (!added) { // What if this is a class folder??? addJavaElementPath(fbProject); } } else if (res instanceof IFile) { // ID: 2734173: allow to analyze classes inside archives if (Util.isClassFile(res) || Util.isJavaArchive(res)) { fbProject.addFile(res.getLocation().toOSString()); } else if (Util.isJavaFile(res)) { addClassesForFile((IFile) res, outputLocations, fbProject); } } else { addJavaElementPath(fbProject); } } private void addJavaElementPath(Project fbProject) { if (javaElt != null) { IPath path = getPath(); if (path != null) { fbProject.addFile(path.toOSString()); } } } public void clearMarkers() throws CoreException { IResource res = getMarkerTarget(); if (javaElt == null || !(res instanceof IProject)) { MarkerUtil.removeMarkers(res); } else { // this is the case of external class folders/libraries: if we would // cleanup ALL project markers, it would also remove markers from // ALL // source/class files, not only for the selected one. IMarker[] allMarkers = MarkerUtil.getAllMarkers(res); Set<IMarker> set = MarkerUtil.findMarkerForJavaElement(javaElt, allMarkers, true); // TODO can be very slow. May think about batch operation w/o // resource notifications // P.S. if executing "clean+build", package explorer first doesn't // notice // any change because the external class file labels are not // refreshed after clean, // but after build we trigger an explicit view refresh, see // FindBugsAction for (IMarker marker : set) { marker.delete(); } } } public IProject getProject() { return project; } public IJavaProject getJavaProject() { return JavaCore.create(project); } /** * @return false if no classes was added */ private static boolean addClassesForFolder(IFolder folder, Map<IPath, IPath> outLocations, Project fbProject) { IPath path = folder.getLocation(); IPath srcRoot = getMatchingSourceRoot(path, outLocations); if (srcRoot == null) { return false; } IPath outputRoot = outLocations.get(srcRoot); int firstSegments = path.matchingFirstSegments(srcRoot); // add relative path to the output path IPath out = outputRoot.append(path.removeFirstSegments(firstSegments)); File directory = out.toFile(); return fbProject.addFile(directory.getAbsolutePath()); // TODO child directories too. Should add preference??? } private static void addClassesForFile(IFile file, Map<IPath, IPath> outLocations, Project fbProject) { IPath path = file.getLocation(); IPath srcRoot = getMatchingSourceRoot(path, outLocations); if (srcRoot == null) { return; } IPath outputRoot = outLocations.get(srcRoot); int firstSegments = path.matchingFirstSegments(srcRoot); // add relative path to the output path IPath out = outputRoot.append(path.removeFirstSegments(firstSegments)); String fileName = path.removeFileExtension().lastSegment(); String namePattern = fileName + "\\.class|" + fileName + "\\$.*\\.class"; namePattern = addSecondaryTypesToPattern(file, fileName, namePattern); File directory = out.removeLastSegments(1).toFile(); // add parent folder and regexp for file names Pattern classNamesPattern = Pattern.compile(namePattern); ResourceUtils.addFiles(fbProject, directory, classNamesPattern); } /** * Add secondary types patterns (not nested in the type itself but contained * in the java file) * * @param fileName * java file name (not path!) without .java suffix * @param classNamePattern * non null pattern for all matching .class file names * @return modified classNamePattern, if there are more then one type * defined in the java file */ private static String addSecondaryTypesToPattern(IFile file, String fileName, String classNamePattern) { ICompilationUnit cu = JavaCore.createCompilationUnitFrom(file); if (cu == null) { FindbugsPlugin.getDefault().logError( "NULL compilation unit for " + file + ", FB analysis might be incomplete for included types"); return classNamePattern; } try { IType[] types = cu.getTypes(); if (types.length > 1) { StringBuilder sb = new StringBuilder(classNamePattern); for (IType type : types) { if (fileName.equals(type.getElementName())) { // "usual" type with the same name: we have it already continue; } sb.append("|").append(type.getElementName()); sb.append("\\.class|").append(type.getElementName()); sb.append("\\$.*\\.class"); } classNamePattern = sb.toString(); } } catch (JavaModelException e) { FindbugsPlugin.getDefault().logException(e, "Cannot get types from compilation unit: " + cu); } return classNamePattern; } public @CheckForNull IResource getCorespondingResource() { if (resource != null) { return resource; } try { IResource resource1 = javaElt.getCorrespondingResource(); if(resource1 != null) { return resource1; } IJavaElement ancestor = javaElt.getAncestor(IJavaElement.COMPILATION_UNIT); if(ancestor != null){ return ancestor.getCorrespondingResource(); } } catch (JavaModelException e) { // ignore, just return nothing } return null; } public @CheckForNull IJavaElement getCorespondingJavaElement() { if (javaElt != null) { return javaElt; } return JavaCore.create(resource); } /** * @return the resource which can be used to attach markers found for this * item. This resource must exist, and the return value can not be * null. The return value can be absolutely unrelated to the * {@link #getCorespondingResource()}. */ public @Nonnull IResource getMarkerTarget() { IResource res = getCorespondingResource(); if (res != null) { return res; } if (javaElt != null) { IResource resource2 = javaElt.getResource(); if (resource2 != null) { return resource2; } } // probably not the best solution, but this should always work return project; } /** * @return number of markers which are <b>already</b> reported for given * work item. */ public int getMarkerCount(boolean recursive) { return getMarkers(recursive).size(); } /** * @return markers which are <b>already</b> reported for given work item */ public Set<IMarker> getMarkers(boolean recursive) { IResource res = getCorespondingResource(); if (res != null) { if (res.getType() == IResource.PROJECT || javaElt instanceof IPackageFragmentRoot) { // for project, depth_one does not make any sense here recursive = true; } IMarker[] markers = MarkerUtil.getMarkers(res, recursive ? IResource.DEPTH_INFINITE : IResource.DEPTH_ONE); return new HashSet<IMarker>(Arrays.asList(markers)); } IResource markerTarget = getMarkerTarget(); if(!markerTarget.isAccessible()) { return Collections.emptySet(); } if (!recursive && ((markerTarget.getType() == IResource.PROJECT && (javaElt instanceof IPackageFragmentRoot) || Util .isClassFile(javaElt)) || (Util.isJavaArchive(markerTarget) && Util.isClassFile(javaElt)))) { recursive = true; } IMarker[] markers = MarkerUtil.getMarkers(markerTarget, recursive ? IResource.DEPTH_INFINITE : IResource.DEPTH_ONE); Set<IMarker> forJavaElement = MarkerUtil.findMarkerForJavaElement(javaElt, markers, recursive); return forJavaElement; } /** * @param srcPath may be null * @param outLocations * key is the source root, value is output folder * @return source root folder matching (parent of) given path, or null */ private static @Nullable IPath getMatchingSourceRoot(@Nullable IPath srcPath, Map<IPath, IPath> outLocations) { if(srcPath == null) { return null; } Set<Entry<IPath, IPath>> outEntries = outLocations.entrySet(); IPath result = null; int maxSegments = 0; for (Entry<IPath, IPath> entry : outEntries) { IPath srcRoot = entry.getKey(); int firstSegments = srcPath.matchingFirstSegments(srcRoot); if (firstSegments > maxSegments && firstSegments == srcRoot.segmentCount()) { maxSegments = firstSegments; result = srcRoot; } } return result; } public String getName() { return resource != null ? resource.getName() : javaElt.getElementName(); } /** * * @return full absolute path corresponding to the work item (file or * directory). If the work item is a part of an achive, it's the * path to the archive file. If the work item is a project, it's the * path to the project root. TODO If the work item is an internal * java element (method, inner class etc), results are undefined * yet. */ public @CheckForNull IPath getPath() { IResource corespondingResource = getCorespondingResource(); if (corespondingResource != null) { return corespondingResource.getLocation(); } if (javaElt != null) { return javaElt.getPath(); } return null; } public boolean isDirectory() { IResource corespondingResource = getCorespondingResource(); if (corespondingResource != null) { return corespondingResource.getType() == IResource.FOLDER || corespondingResource.getType() == IResource.PROJECT; } return false; } /** * * @return true if the given element is contained inside archive */ public boolean isFromArchive() { IPath path = getPath(); if (path == null) { return false; } File file = path.toFile(); if (file.isDirectory()) { return false; } return Archive.isArchiveFileName(file.getName()); } @Override public String toString() { return getName(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((javaElt == null) ? 0 : javaElt.hashCode()); result = prime * result + ((resource == null) ? 0 : resource.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof WorkItem)) { return false; } WorkItem other = (WorkItem) obj; if (javaElt == null) { if (other.javaElt != null) { return false; } } else if (!javaElt.equals(other.javaElt)) { return false; } if (resource == null) { if (other.resource != null) { return false; } } else if (!resource.equals(other.resource)) { return false; } return true; } }