/******************************************************************************* * 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) { 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); } } } }