| /******************************************************************************* |
| * Copyright (c) 2015, 2016 GK Software AG. |
| * 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: |
| * Stephan Herrmann - initial API and implementation |
| * Lars Vogel <Lars.Vogel@vogella.com> - Contributions for |
| * Bug 473178 |
| *******************************************************************************/ |
| package org.eclipse.jdt.internal.core; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import org.eclipse.core.resources.IResourceChangeEvent; |
| import org.eclipse.core.resources.IResourceChangeListener; |
| import org.eclipse.core.resources.IResourceDelta; |
| import org.eclipse.core.resources.IWorkspace; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.jdt.core.IPackageFragmentRoot; |
| import org.eclipse.jdt.core.JavaModelException; |
| import org.eclipse.jdt.internal.compiler.classfmt.ExternalAnnotationProvider; |
| import org.eclipse.jdt.internal.core.util.Util; |
| |
| /** |
| * Track changes of external annotation files and trigger closing / reloading of affected ClassFiles. |
| */ |
| public class ExternalAnnotationTracker implements IResourceChangeListener { |
| |
| /** |
| * Nodes in a tree that represents external annotation attachments. |
| * Each node is either an intermediate node, or an annotation base. |
| * <p> |
| * <b>Intermediate nodes</b> represent the workspace structure holding the |
| * external annotations. They may have children.<br/> |
| * <em>Note: we don't flatten these intermediate nodes as to facilitate |
| * matching against the exact structure of resource deltas.</em> |
| * </p><p> |
| * An <b>annotation base</b> is a leaf in the represented directory structure |
| * and may have a map of known class files. |
| * </p> |
| */ |
| static class DirectoryNode { |
| |
| DirectoryNode parent; |
| IPath path; |
| |
| /** Key is a full workspace path. */ |
| Map<IPath,DirectoryNode> children; |
| /** |
| * Key is the path of an external annotation file (.eea), relative to this annotation base. |
| * The annotation file need not exist, in which case we are waiting for its creation. |
| */ |
| Map<IPath, ClassFile> classFiles; |
| IPackageFragmentRoot modelRoot; // TODO: for handling zipped annotations |
| |
| public DirectoryNode(DirectoryNode parent, IPath path) { |
| this.parent = parent; |
| this.path = path; |
| } |
| |
| Map<IPath, DirectoryNode> getChildren() { |
| if (this.children == null) |
| this.children = new HashMap<>(); |
| return this.children; |
| } |
| |
| void registerClassFile(IPath relativeAnnotationPath, ClassFile classFile) { |
| if (this.classFiles == null) |
| this.classFiles = new HashMap<>(); |
| this.classFiles.put(relativeAnnotationPath, classFile); |
| if (this.modelRoot == null) |
| this.modelRoot = classFile.getPackageFragmentRoot(); |
| } |
| |
| void unregisterClassFile(IPath relativeAnnotationPath) { |
| if (this.classFiles != null) { |
| this.classFiles.remove(relativeAnnotationPath); |
| if (this.classFiles.isEmpty() && this.parent != null) |
| this.parent.unregisterDirectory(this); |
| } |
| } |
| void unregisterDirectory(DirectoryNode child) { |
| if (this.children != null) |
| this.children.remove(child.path); |
| if ((this.children == null || this.children.isEmpty()) && this.parent != null) |
| this.parent.unregisterDirectory(this); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuffer buf = new StringBuffer(); |
| if (this.classFiles != null) |
| buf.append("annotation base "); //$NON-NLS-1$ |
| buf.append("directory\n"); //$NON-NLS-1$ |
| if (this.children != null) |
| buf.append("\twith ").append(this.children.size()).append(" children\n"); //$NON-NLS-1$ //$NON-NLS-2$ |
| buf.append("\t#classFiles: ").append(numClassFiles()); //$NON-NLS-1$ |
| return buf.toString(); |
| } |
| int numClassFiles() { |
| if (this.classFiles != null) |
| return this.classFiles.size(); |
| int count = 0; |
| if (this.children != null) |
| for (DirectoryNode child : this.children.values()) |
| count += child.numClassFiles(); |
| return count; |
| } |
| boolean isEmpty() { |
| return (this.children == null || this.children.isEmpty()) && (this.classFiles == null || this.classFiles.isEmpty()); |
| } |
| } |
| |
| /** The tree of tracked annotation bases and class files. */ |
| DirectoryNode tree = new DirectoryNode(null, null); |
| |
| private static ExternalAnnotationTracker singleton; |
| private ExternalAnnotationTracker() { } |
| |
| /** Start listening. */ |
| static void start(IWorkspace workspace) { |
| singleton = new ExternalAnnotationTracker(); |
| workspace.addResourceChangeListener(singleton); |
| } |
| |
| /** Stop listening & clean up. */ |
| static void shutdown(IWorkspace workspace) { |
| if (singleton != null) { |
| workspace.removeResourceChangeListener(singleton); |
| singleton.tree.children = null; |
| } |
| } |
| |
| /** |
| * Register a ClassFile, to which the annotation attachment 'annotationBase' applies. |
| * This is done for the purpose to listen to changes in the corresponding external annotations |
| * and to force reloading the class file when necessary. |
| * @param annotationBase the path of the annotation attachment (workspace absolute) |
| * @param relativeAnnotationPath path corresponding to the qualified name of the main type of the class file. |
| * The path is relative to 'annotationBase'. |
| * When appending the file extension for annotation files it points to the annotation file |
| * that would correspond to the given class file. The annotation file may or may not yet exist. |
| * @param classFile the ClassFile to register. |
| */ |
| public static void registerClassFile(IPath annotationBase, IPath relativeAnnotationPath, ClassFile classFile) { |
| int baseDepth = annotationBase.segmentCount(); |
| if (baseDepth == 0) { |
| Util.log(new IllegalArgumentException("annotationBase cannot be empty")); //$NON-NLS-1$ |
| } else { |
| relativeAnnotationPath = relativeAnnotationPath.addFileExtension(ExternalAnnotationProvider.ANNOTATION_FILE_EXTENSION); |
| DirectoryNode base = singleton.getAnnotationBase(singleton.tree, annotationBase, baseDepth, 1); |
| base.registerClassFile(relativeAnnotationPath, classFile); |
| } |
| } |
| |
| /** |
| * Unregister a class file that is being closed. |
| * Only to be invoked for class files that potentially are affected by external annotations. |
| * @param annotationBase path of the corresponding annotation attachment (workspace absolute) |
| * @param relativeAnnotationPath path of the annotation file that would correspond to the given class file. |
| */ |
| public static void unregisterClassFile(IPath annotationBase, IPath relativeAnnotationPath) { |
| int baseDepth = annotationBase.segmentCount(); |
| if (baseDepth == 0) { |
| Util.log(new IllegalArgumentException("annotationBase cannot be empty")); //$NON-NLS-1$ |
| } else { |
| relativeAnnotationPath = relativeAnnotationPath.addFileExtension(ExternalAnnotationProvider.ANNOTATION_FILE_EXTENSION); |
| DirectoryNode base = singleton.getAnnotationBase(singleton.tree, annotationBase, baseDepth, 1); |
| base.unregisterClassFile(relativeAnnotationPath); |
| } |
| } |
| |
| private DirectoryNode getAnnotationBase(DirectoryNode current, IPath annotationBase, int baseDepth, int nextDepth) { |
| IPath nextHead = annotationBase.uptoSegment(nextDepth); |
| Map<IPath, DirectoryNode> children = current.getChildren(); // create if necessary |
| DirectoryNode nextHeadNode = children.get(nextHead); |
| if (nextHeadNode == null) |
| children.put(nextHead, nextHeadNode = new DirectoryNode(current, nextHead)); |
| if (baseDepth == nextDepth) |
| return nextHeadNode; |
| return getAnnotationBase(nextHeadNode, annotationBase, baseDepth, nextDepth+1); |
| } |
| |
| /** |
| * Listen to resource change events concerning external annotations, that potentially affect a cached ClassFile. |
| */ |
| @Override |
| public void resourceChanged(IResourceChangeEvent event) { |
| IResourceDelta delta = event.getDelta(); |
| if (delta != null && delta.getFullPath().isRoot() && this.tree.children != null) { |
| for (IResourceDelta child : delta.getAffectedChildren()) { |
| DirectoryNode directoryNode = this.tree.children.get(child.getFullPath()); |
| if (directoryNode != null) |
| traverseForDirectories(directoryNode, child); |
| } |
| } |
| } |
| |
| // co-traversal of directory nodes & delta nodes: |
| private void traverseForDirectories(DirectoryNode directoryNode, IResourceDelta matchedDelta) { |
| if (directoryNode.classFiles != null) { |
| // annotation base reached, switch strategy: |
| traverseForClassFiles(directoryNode.classFiles, matchedDelta, matchedDelta.getFullPath().segmentCount()); |
| // ignore further children, if we already have classFiles (i.e., nested annotation bases are tolerated but ignored). |
| } else if (directoryNode.children != null) { |
| for (IResourceDelta child : matchedDelta.getAffectedChildren()) { |
| DirectoryNode childDir = directoryNode.children.get(child.getFullPath()); |
| if (childDir != null) { |
| if (child.getKind() == IResourceDelta.REMOVED) |
| directoryNode.children.remove(child.getFullPath()); |
| else |
| traverseForDirectories(childDir, child); |
| } |
| } |
| } |
| if (directoryNode.isEmpty()) |
| directoryNode.parent.children.remove(matchedDelta.getFullPath()); |
| } |
| |
| // traversal of delta nodes to be matched against map of class files: |
| private void traverseForClassFiles(Map<IPath, ClassFile> classFiles, IResourceDelta matchedDelta, int baseDepth) { |
| for (IResourceDelta delta : matchedDelta.getAffectedChildren()) { |
| IPath deltaRelativePath = delta.getFullPath().removeFirstSegments(baseDepth); |
| ClassFile classFile = classFiles.remove(deltaRelativePath); |
| if (classFile != null) { |
| try { |
| // the payload: unload the class file corresponding to a changed external annotation file: |
| classFile.closeAndRemoveFromJarTypeCache(); |
| } catch (JavaModelException e) { |
| Util.log(e, "Failed to close ClassFile "+classFile.name); //$NON-NLS-1$ |
| } |
| } else { |
| traverseForClassFiles(classFiles, delta, baseDepth); |
| } |
| } |
| } |
| } |