/******************************************************************************* * Copyright (c) 2008, 2010 Wind River Systems, Inc. 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: * Markus Schorn - initial API and implementation *******************************************************************************/ package org.eclipse.cdt.internal.core.resources; import java.lang.ref.SoftReference; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.cdt.core.CCorePlugin; import org.eclipse.cdt.core.CProjectNature; import org.eclipse.cdt.core.model.CoreModel; import org.eclipse.cdt.core.parser.util.CharArrayUtils; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceDeltaVisitor; import org.eclipse.core.resources.IResourceProxy; import org.eclipse.core.resources.IResourceProxyVisitor; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.content.IContentType; import org.eclipse.core.runtime.content.IContentTypeManager; import org.eclipse.core.runtime.jobs.Job; /** * Allows for looking up resources by location or name. When using this class 100 bytes per resource * are needed. Therefore the support is limited to header-files int non-cdt projects and all files * except non-cdt-files in CDT projects. * * The information for a project is initialized when first requested and then it is kept up to date * using a resource change listener. No memory is used, as long as the class is not used. * When information is not used for more than 10 minutes, the data-structures will be held via a weak * reference, only and are subject to garbage collection. * * The node map stores a map from hash-code of file-names to nodes. * A node contains the name of a file plus a link to the parent resource. From that we can compute * the resource path and obtain further information via the resource. */ class ResourceLookupTree implements IResourceChangeListener, IResourceDeltaVisitor, IResourceProxyVisitor { private static final int UNREF_DELAY = 10 * 60000; // 10 min private static final boolean VISIT_CHILDREN = true; private static final boolean SKIP_CHILDREN = false; private static final IFile[] NO_FILES = new IFile[0]; private static final int TRIGGER_RECALC= IResourceDelta.TYPE | IResourceDelta.REPLACED | IResourceDelta.LOCAL_CHANGED | IResourceDelta.OPEN; private static class Extensions { private final boolean fInvert; private final Set<String> fExtensions; Extensions(Set<String> extensions, boolean invert) { fInvert= invert; fExtensions= extensions; } boolean isRelevant(String filename) { // accept all files without extension final int idx= filename.lastIndexOf('.'); if (idx < 0) return true; return fExtensions.contains(filename.substring(idx+1).toUpperCase()) != fInvert; } } private static class Node { final Node fParent; final char[] fResourceName; final boolean fHasFileLocationName; final boolean fIsFileLinkTarget; boolean fDeleted; boolean fHasChildren; int fCanonicHash; Node(Node parent, char[] name, boolean hasFileLocationName, boolean isFileLinkTarget) { fParent= parent; fResourceName= name; fHasFileLocationName= hasFileLocationName; fIsFileLinkTarget= isFileLinkTarget; if (parent != null) parent.fHasChildren= true; } } private final Object fLock= new Object(); private final Job fUnrefJob; private SoftReference<Map<Integer, Object>> fNodeMapRef; private Map<Integer, Object> fNodeMap; private final Map<String, Extensions> fFileExtensions; private Extensions fCDTProjectExtensions; private Extensions fDefaultExtensions; private Extensions fCurrentExtensions; private Node fRootNode; private boolean fNeedCleanup; private Node fLastFolderNode; public ResourceLookupTree() { fRootNode= new Node(null, CharArrayUtils.EMPTY, false, false) {}; fFileExtensions= new HashMap<String, Extensions>(); fUnrefJob= new Job("Timer") { //$NON-NLS-1$ @Override protected IStatus run(IProgressMonitor monitor) { unrefNodeMap(); return Status.OK_STATUS; } }; fUnrefJob.setSystem(true); } public void startup() { final IWorkspace workspace = ResourcesPlugin.getWorkspace(); workspace.addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE); } public void shutdown() { ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); synchronized (fLock) { fNodeMap= null; fNodeMapRef= null; fFileExtensions.clear(); } } /** * Handle resource change notifications. */ public void resourceChanged(IResourceChangeEvent event) { IResourceDelta delta= event.getDelta(); synchronized (fLock) { if (fNodeMapRef == null) return; boolean unsetMap= false; if (fNodeMap == null) { fNodeMap= fNodeMapRef.get(); if (fNodeMap == null) return; unsetMap= true; } try { delta.accept(this); } catch (CoreException e) { CCorePlugin.log(e); } finally { if (fNeedCleanup) cleanup(); fCurrentExtensions= null; fNeedCleanup= false; if (unsetMap) fNodeMap= null; } } } /** * Handles resource change notifications by visiting the delta. */ public boolean visit(IResourceDelta delta) throws CoreException { assert Thread.holdsLock(fLock); final IResource res= delta.getResource(); if (res instanceof IWorkspaceRoot) return VISIT_CHILDREN; if (res instanceof IProject) { // project not yet handled final String name = res.getName(); final Extensions exts= fFileExtensions.get(name); if (exts == null) return SKIP_CHILDREN; switch (delta.getKind()) { case IResourceDelta.ADDED: // new projects should not yet be part of the tree case IResourceDelta.REMOVED: fFileExtensions.remove(name); remove(res); return SKIP_CHILDREN; case IResourceDelta.CHANGED: if ((delta.getFlags() & (TRIGGER_RECALC | IResourceDelta.DESCRIPTION)) != 0) { fFileExtensions.remove(name); remove(res); return SKIP_CHILDREN; } break; } fCurrentExtensions= exts; return VISIT_CHILDREN; } // file or folder switch (delta.getKind()) { case IResourceDelta.ADDED: add(res); return SKIP_CHILDREN; case IResourceDelta.CHANGED: if ((delta.getFlags() & TRIGGER_RECALC) != 0) { remove(res); add(res); return SKIP_CHILDREN; } return VISIT_CHILDREN; case IResourceDelta.REMOVED: remove(res); return SKIP_CHILDREN; } return VISIT_CHILDREN; } /** * Add a resource to the tree. */ private void add(IResource res) { assert Thread.holdsLock(fLock); if (res instanceof IFile) { final String resName = res.getName(); String linkedName= null; if (res.isLinked()) { URI uri= res.getLocationURI(); if (uri != null) { linkedName= LocationAdapter.URI.extractName(uri); if (linkedName.length() > 0 && fCurrentExtensions.isRelevant(linkedName)) { if (linkedName.equals(resName)) { createFileNode(res.getFullPath(), null); } else { createFileNode(res.getFullPath(), linkedName); } } } } else if (fCurrentExtensions.isRelevant(resName)) { createFileNode(res.getFullPath(), null); } } else { try { res.accept(this, 0); } catch (CoreException e) { CCorePlugin.log(e); } } } /** * Add a resource tree by using a resource proxy visitor. */ public boolean visit(IResourceProxy proxy) throws CoreException { if (proxy.getType() == IResource.FILE) { if (fCurrentExtensions.isRelevant(proxy.getName())) { if (proxy.isLinked()) { IResource res= proxy.requestResource(); if (res instanceof IFile) { add(res); } return true; } createFileNode(proxy.requestFullPath(), null); } } return true; } public void unrefNodeMap() { synchronized (fLock) { fNodeMap= null; } } public void simulateNodeMapCollection() { synchronized (fLock) { fNodeMap= null; fNodeMapRef= new SoftReference<Map<Integer, Object>>(null); } } /** * Initializes nodes for the given projects. Also creates the node map if it was collected. */ private void initializeProjects(IProject[] projects) { assert Thread.holdsLock(fLock); if (fNodeMap == null) { if (fNodeMapRef != null) { fNodeMap= fNodeMapRef.get(); } if (fNodeMap == null) { fFileExtensions.clear(); fNodeMap= new HashMap<Integer, Object>(); fNodeMapRef= new SoftReference<Map<Integer, Object>>(fNodeMap); } } fUnrefJob.cancel(); fUnrefJob.schedule(UNREF_DELAY); for (IProject project : projects) { if (project.isOpen() && !fFileExtensions.containsKey(project.getName())) { Extensions ext= fDefaultExtensions; try { if (project.hasNature(CProjectNature.C_NATURE_ID)) { ext= fCDTProjectExtensions; } } catch (CoreException e) { CCorePlugin.log(e); // treat as non-cdt project } fCurrentExtensions= ext; add(project); fFileExtensions.put(project.getName(), ext); fCurrentExtensions= null; } } } /** * Initializes file-extensions and node map */ private void initFileExtensions() { if (fDefaultExtensions == null) { HashSet<String> cdtContentTypes= new HashSet<String>(); String[] registeredContentTypes= CoreModel.getRegistedContentTypeIds(); cdtContentTypes.addAll(Arrays.asList(registeredContentTypes)); final IContentTypeManager ctm= Platform.getContentTypeManager(); final IContentType[] ctts= ctm.getAllContentTypes(); Set<String> cdtExtensions= new HashSet<String>(); for (IContentType ctt : ctts) { IContentType basedOn= ctt; while (basedOn != null) { if (cdtContentTypes.contains(basedOn.getId())) { addFileSpecs(ctt, cdtExtensions); break; } basedOn= basedOn.getBaseType(); } } fDefaultExtensions= new Extensions(cdtExtensions, false); Set<String> nonCDTExtensions= new HashSet<String>(); outer: for (IContentType ctt : ctts) { IContentType basedOn= ctt; while (basedOn != null) { if (cdtContentTypes.contains(basedOn.getId())) continue outer; basedOn= basedOn.getBaseType(); } // this is a non-cdt content type addFileSpecs(ctt, nonCDTExtensions); } // Bug 323659: In case there is another content type for a cdt file-extension we need // to remove it. nonCDTExtensions.removeAll(cdtExtensions); fCDTProjectExtensions= new Extensions(nonCDTExtensions, true); } } private void addFileSpecs(IContentType ctt, Set<String> result) { String[] fspecs= ctt.getFileSpecs(IContentType.FILE_EXTENSION_SPEC); for (String fspec : fspecs) { result.add(fspec.toUpperCase()); } } /** * Inserts a node for the given path. */ private void createFileNode(IPath fullPath, String fileLink) { final String[] segments= fullPath.segments(); final boolean isFileLinkTarget= fileLink != null; final char[][] charArraySegments = toCharArrayArray(segments, fileLink); createNode(charArraySegments, charArraySegments.length, true, isFileLinkTarget); } private char[][] toCharArrayArray(String[] segments, String fileLink) { final int segmentLen = segments.length; char[][] chsegs; if (fileLink != null) { chsegs= new char[segmentLen+1][]; chsegs[segmentLen]= fileLink.toCharArray(); } else { chsegs= new char[segmentLen][]; } for (int i = 0; i < segmentLen; i++) { chsegs[i]= segments[i].toCharArray(); } return chsegs; } /** * Inserts a node for the given path. */ private Node createNode(char[][] segments, int segmentCount, boolean hasFileLocationName, boolean isFileLinkTarget) { assert Thread.holdsLock(fLock); if (segmentCount == 0) return fRootNode; if (!hasFileLocationName && fLastFolderNode != null) { if (isNodeForSegments(fLastFolderNode, segments, segmentCount, isFileLinkTarget)) return fLastFolderNode; } final char[] name= segments[segmentCount-1]; final int hash= hashCode(name); // search for existing node Object obj= fNodeMap.get(hash); Node[] nodes= null; int len= 0; if (obj != null) { if (obj instanceof Node) { Node node= (Node) obj; if (isNodeForSegments(node, segments, segmentCount, isFileLinkTarget)) { if (!hasFileLocationName) fLastFolderNode= node; return node; } nodes= new Node[]{node, null}; fNodeMap.put(hash, nodes); len= 1; } else { nodes= (Node[]) obj; for (len=0; len < nodes.length; len++) { Node node = nodes[len]; if (node == null) break; if (isNodeForSegments(node, segments, segmentCount, isFileLinkTarget)) { if (!hasFileLocationName) fLastFolderNode= node; return node; } } } } final Node parent= createNode(segments, segmentCount-1, false, false); Node node= new Node(parent, name, hasFileLocationName, isFileLinkTarget); if (nodes == null) { fNodeMap.put(hash, node); } else { if (len == nodes.length) { Node[] newNodes= new Node[len+2]; System.arraycopy(nodes, 0, newNodes, 0, len); nodes= newNodes; fNodeMap.put(hash, nodes); } nodes[len]= node; } if (!hasFileLocationName) fLastFolderNode= node; return node; } /** * Checks whether the given node matches the given segments. */ private boolean isNodeForSegments(Node node, char[][] segments, int segmentLength, boolean isFileLinkTarget) { assert Thread.holdsLock(fLock); if (node.fIsFileLinkTarget != isFileLinkTarget) return false; while(segmentLength > 0 && node != null) { if (!CharArrayUtils.equals(segments[--segmentLength], node.fResourceName)) return false; node= node.fParent; } return node == fRootNode; } /** * Remove a resource from the tree */ private void remove(IResource res) { assert Thread.holdsLock(fLock); final char[] name= res.getName().toCharArray(); final int hash= hashCode(name); Object obj= fNodeMap.get(hash); if (obj == null) return; final IPath fullPath= res.getFullPath(); final int segmentCount= fullPath.segmentCount(); if (segmentCount == 0) return; final char[][]segments= toCharArrayArray(fullPath.segments(), null); if (obj instanceof Node) { final Node node= (Node) obj; if (!node.fDeleted && isNodeForSegments(node, segments, segmentCount, false)) { node.fDeleted= true; if (node.fHasChildren) fNeedCleanup= true; fNodeMap.remove(hash); } } else { final Node[] nodes= (Node[]) obj; for (int i= 0; i < nodes.length; i++) { Node node = nodes[i]; if (node == null) return; if (!node.fDeleted && isNodeForSegments(node, segments, segmentCount, false)) { remove(nodes, i); if (nodes[0] == null) fNodeMap.remove(hash); node.fDeleted= true; if (node.fHasChildren) fNeedCleanup= true; return; } } } } private void remove(Node[] nodes, int i) { int idx= lastValid(nodes, i); if (idx > 0) { nodes[i]= nodes[idx]; nodes[idx]= null; } } private int lastValid(Node[] nodes, int left) { int right= nodes.length-1; while (left < right) { int mid= (left+right+1)/2; // ==> mid > left if (nodes[mid] == null) right= mid-1; else left= mid; } return right; } private void cleanup() { assert Thread.holdsLock(fLock); fLastFolderNode= null; for (Iterator<Object> iterator = fNodeMap.values().iterator(); iterator.hasNext();) { Object obj= iterator.next(); if (obj instanceof Node) { if (isDeleted((Node) obj)) { iterator.remove(); } } else { Node[] nodes= (Node[]) obj; int j= 0; for (int i = 0; i < nodes.length; i++) { final Node node = nodes[i]; if (node == null) { if (j==0) { iterator.remove(); } break; } if (!isDeleted(node)) { if (i != j) { nodes[j]= node; nodes[i]= null; } j++; } else { nodes[i]= null; } } } } } private boolean isDeleted(Node node) { while(node != null) { if (node.fDeleted) return true; node= node.fParent; } return false; } /** * Computes a case insensitive hash-code for file names. */ private int hashCode(char[] name) { int h= 0; final int len = name.length; for (int i = 0; i < len; i++) { h = 31*h + Character.toUpperCase(name[i]); } return h; } /** * Searches for all files with the given location. In case the name of the location is * a cdt-content type the lookup tree is consulted, otherwise as a fallback the platform's * method is called. */ public IFile[] findFilesForLocationURI(URI location) { return findFilesForLocation(location, LocationAdapter.URI); } /** * Searches for all files with the given location. In case the name of the location is * a cdt-content type the lookup tree is consulted, otherwise as a fallback the platform's * method is called. */ public IFile[] findFilesForLocation(IPath location) { return findFilesForLocation(location, LocationAdapter.PATH); } /** * Searches for all files with the given location. In case the name of the location is * a cdt-content type the lookup tree is consulted, otherwise as a fallback the platform's * method is called. */ public <T> IFile[] findFilesForLocation(T location, LocationAdapter<T> adapter) { initFileExtensions(); String name= adapter.extractName(location); Node[] candidates= null; synchronized (fLock) { initializeProjects(ResourcesPlugin.getWorkspace().getRoot().getProjects()); Object obj= fNodeMap.get(hashCode(name.toCharArray())); if (obj != null) { candidates= convert(obj); IFile[] result= extractMatchesForLocation(candidates, location, adapter); if (result.length > 0) return result; } } // fall back to platform functionality return adapter.platformsFindFilesForLocation(location); } private Node[] convert(Object obj) { if (obj instanceof Node) return new Node[] {(Node) obj}; final Node[] nodes= (Node[]) obj; final int len= lastValid(nodes, -1)+1; final Node[] result= new Node[len]; System.arraycopy(nodes, 0, result, 0, len); return result; } /** * Returns an array of files for the given name. Search is limited to the supplied projects. */ public IFile[] findFilesByName(IPath relativeLocation, IProject[] projects, boolean ignoreCase) { final int segCount= relativeLocation.segmentCount(); if (segCount < 1) return NO_FILES; final String name= relativeLocation.lastSegment(); Node[] candidates; initFileExtensions(); synchronized (fLock) { initializeProjects(projects); Object obj= fNodeMap.get(hashCode(name.toCharArray())); if (obj == null) { return NO_FILES; } candidates= convert(obj); } String suffix= relativeLocation.toString(); while(suffix.startsWith("../")) { //$NON-NLS-1$ suffix= suffix.substring(3); } Set<String> prjset= new HashSet<String>(); for (IProject prj : projects) { prjset.add(prj.getName()); } return extractMatchesForName(candidates, name, suffix, ignoreCase, prjset); } /** * Selects the actual matches for the list of candidate nodes. */ private IFile[] extractMatchesForName(Node[] candidates, String name, String suffix, boolean ignoreCase, Set<String> prjSet) { final char[] n1= name.toCharArray(); final int namelen = n1.length; int resultIdx= 0; if (ignoreCase) { for (int j = 0; j < namelen; j++) { n1[j]= Character.toUpperCase(n1[j]); } } final int suffixLen= suffix.length(); final IWorkspaceRoot root= ResourcesPlugin.getWorkspace().getRoot(); IFile[] result= null; outer: for (int i = 0; i < candidates.length; i++) { final Node node = candidates[i]; if (node.fHasFileLocationName && checkProject(node, prjSet)) { final char[] n2= node.fResourceName; if (namelen == n2.length) { for (int j = 0; j < n2.length; j++) { final char c= ignoreCase ? Character.toUpperCase(n2[j]) : n2[j]; if (c != n1[j]) continue outer; } final IFile file= root.getFile(createPath(node)); final URI loc= file.getLocationURI(); if (loc != null) { String path= loc.getPath(); final int len= path.length(); if (len >= suffixLen && suffix.regionMatches(ignoreCase, 0, path, len-suffixLen, suffixLen)) { if (result == null) result= new IFile[candidates.length-i]; result[resultIdx++]= root.getFile(createPath(node)); } } } } } if (result==null) return NO_FILES; if (resultIdx < result.length) { IFile[] copy= new IFile[resultIdx]; System.arraycopy(result, 0, copy, 0, resultIdx); return copy; } return result; } private boolean checkProject(Node node, Set<String> prjSet) { while(true) { final Node n= node.fParent; if (n == fRootNode) break; if (n == null) return false; node= n; } return prjSet.contains(new String(node.fResourceName)); } private IPath createPath(Node node) { if (node == fRootNode) return Path.ROOT; if (node.fIsFileLinkTarget) return createPath(node.fParent); return createPath(node.fParent).append(new String(node.fResourceName)); } /** * Selects the actual matches from the list of candidates */ private <T> IFile[] extractMatchesForLocation(Node[] candidates, T location, LocationAdapter<T> adapter) { final IWorkspaceRoot root= ResourcesPlugin.getWorkspace().getRoot(); final String searchPath= adapter.getCanonicalPath(location); IFile[] result= null; int resultIdx= 0; for (int i = 0; i < candidates.length; i++) { final Node node = candidates[i]; if (node.fHasFileLocationName) { final IFile file= root.getFile(createPath(node)); final T loc= adapter.getLocation(file); if (loc != null) { if (!loc.equals(location)) { if (searchPath == null) continue; if (node.fCanonicHash != 0 && node.fCanonicHash != searchPath.hashCode()) continue; final String candPath= adapter.getCanonicalPath(loc); if (candPath == null) continue; node.fCanonicHash= candPath.hashCode(); if (!candPath.equals(searchPath)) continue; } if (result == null) result= new IFile[candidates.length-i]; result[resultIdx++]= root.getFile(createPath(node)); } } } if (result==null) return NO_FILES; if (resultIdx < result.length) { IFile[] copy= new IFile[resultIdx]; System.arraycopy(result, 0, copy, 0, resultIdx); return copy; } return result; } @SuppressWarnings("nls") public void dump() { List<String> lines= new ArrayList<String>(); synchronized (fLock) { for (Object object : fNodeMap.values()) { Node[] nodes= convert(object); for (final Node node : nodes) { if (node == null) { break; } lines.add(toString(node)); } } } Collections.sort(lines); System.out.println("Dumping files:"); for (String line : lines) { System.out.println(line); } System.out.flush(); } @SuppressWarnings("nls") private String toString(Node node) { if (node == fRootNode) return ""; return toString(node.fParent) + "/" + new String(node.fResourceName); } }