/* * Copyright 2013 Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.tools.core.internal.builder; import com.google.common.base.Objects; import com.google.dart.engine.context.AnalysisContext; import com.google.dart.engine.source.DirectoryBasedSourceContainer; import com.google.dart.engine.source.FileBasedSource; import com.google.dart.engine.source.Source; import com.google.dart.engine.source.SourceContainer; import com.google.dart.tools.core.DartCore; import com.google.dart.tools.core.DartCoreDebug; import com.google.dart.tools.core.analysis.model.Project; import com.google.dart.tools.core.analysis.model.PubFolder; import com.google.dart.tools.core.internal.analysis.model.InvertedSourceContainer; import static com.google.dart.tools.core.DartCore.PACKAGES_DIRECTORY_NAME; import static com.google.dart.tools.core.DartCore.PUBSPEC_FILE_NAME; import static com.google.dart.tools.core.DartCore.isDartLikeFileName; import static com.google.dart.tools.core.DartCore.isHtmlLikeFileName; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; 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.runtime.CoreException; import org.eclipse.core.runtime.IPath; import static org.eclipse.core.resources.IResource.FILE; import static org.eclipse.core.resources.IResource.PROJECT; import static org.eclipse.core.resources.IResourceDelta.ADDED; import static org.eclipse.core.resources.IResourceDelta.CHANGED; import static org.eclipse.core.resources.IResourceDelta.REMOVED; import java.io.File; import java.io.IOException; /** * {@code DeltaProcessor} traverses both {@link IResource} hierarchies and {@link IResourceDelta}s. * As Dart related resources are encountered, registered {@link DeltaListener}s are notified. * * @coverage dart.tools.core.builder */ public class DeltaProcessor extends DeltaBroadcaster { private class Event implements SourceDeltaEvent, SourceContainerDeltaEvent { private IResourceProxy proxy; private IResource resource; private File canonicalPackageDir; private IPath fullPackagePath; private Source source; private SourceContainer sourceContainer; private AnalysisContext packagesRemovedFromContext; private String packagesRemovedFromContextId; @Override public AnalysisContext getContext() { return context; } @Override public String getContextId() { return contextId; } @Override public Project getProject() { return project; } @Override public PubFolder getPubFolder() { return pubFolder; } @Override public IResource getResource() { if (resource == null && proxy != null) { resource = proxy.requestResource(); } return resource; } @Override public Source getSource() { if (source == null) { File file = getResourceFile(); if (file != null) { source = new FileBasedSource(file); } else { logNoLocation(getResource()); } } return source; } @Override public SourceContainer getSourceContainer() { if (sourceContainer == null) { File file = getResourceFile(); if (file != null) { sourceContainer = new DirectoryBasedSourceContainer(file); } else { logNoLocation(getResource()); } } return sourceContainer; } @Override public boolean isTopContainerInContext() { if (resource != null || proxy != null) { return DeltaProcessor.this.isTopContainerInContext((IContainer) getResource()); } else { return false; } } /** * Answer the file associated with the given resource, ensuring that the file is canonical if it * resides in a package. * * @return the file or {@code null} if the resource location cannot be determined */ File getResourceFile() { if (fullPackagePath != null) { IPath fullPath = getResource().getFullPath(); return new File(canonicalPackageDir, fullPath.makeRelativeTo(fullPackagePath).toString()); } IPath location = getResource().getLocation(); if (location != null) { return location.toFile(); } return null; } /** * Configure the event to represent that a package or the entire "packages" directory was * removed. Because the original canonical location is lost (the symlink has been removed), we * represent this by an {@link InvertedSourceContainer "inverted" source container} that * contains everything *not* currently in the context's root folder or any of the existing * canonical package folders. Because this effectively represents all removed packages, we only * fire this event at most once per delta. * * @return {@code true} if the * {@link DeltaListener#packageSourceContainerRemoved(SourceContainerDeltaEvent) package * removed event} should be fired, or {@code false} if it has already been fired earlier * when processing the current delta. */ boolean setPackagesRemoved() { this.proxy = null; this.resource = null; this.canonicalPackageDir = null; this.fullPackagePath = null; this.source = null; PubFolder pubFolder = getPubFolder(); this.sourceContainer = pubFolder == null ? null : pubFolder.getInvertedSourceContainer(); if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { if (Objects.equal(packagesRemovedFromContextId, contextId)) { return false; } this.packagesRemovedFromContextId = contextId; } else { if (packagesRemovedFromContext == context) { return false; } this.packagesRemovedFromContext = context; } return true; } /** * Configure the event to represent the specified resource * * @param proxy the resource proxy (not {@code null}) * @param canonicalPackageDir the canonical package directory or {@code null} if the resource is * not in a package directory * @param fullPackagePath the full Eclipse path of the package directory ( * {@link IResource#getFullPath()} or {@link IResourceProxy#requestFullPath()}) or null * if the resource is not in a package directory */ void setProxy(IResourceProxy proxy, File canonicalPackageDir, IPath fullPackagePath) { this.proxy = proxy; this.resource = null; this.canonicalPackageDir = canonicalPackageDir; this.fullPackagePath = fullPackagePath; this.source = null; this.sourceContainer = null; } /** * Configure the event to represent the specified resource * * @param resource the resource (not {@code null}) * @param canonicalPackageDir the canonical package directory or {@code null} if the resource is * not in a package directory * @param fullPackagePath the full Eclipse path of the package directory ( * {@link IResource#getFullPath()} or {@link IResourceProxy#requestFullPath()}) or null * if the resource is not in a package directory */ void setResource(IResource resource, File canonicalPackageDir, IPath fullPackagePath) { this.proxy = null; this.resource = resource; this.canonicalPackageDir = canonicalPackageDir; this.fullPackagePath = fullPackagePath; this.source = null; this.sourceContainer = null; } } private final Project project; private PubFolder pubFolder; private AnalysisContext context; private String contextId; private Event event; /** * Construct a new instance for traversing project resources * * @param project the project containing the resources being traversed (not {@code null}) */ public DeltaProcessor(Project project) { this.project = project; } /** * Traverse the specified resources in the associated {@link Project}. Listeners will be notified * of Dart related resources. * * @param resource the container of resources to be recursively traversed (not {@code null}) */ public void traverse(IResource resource) throws CoreException { event = new Event(); if (resource instanceof IFile) { setContextFor(resource.getParent()); } processResources(resource); event = null; } /** * Traverse the specified resource changes in the associated {@link Project}. Listeners will be * notified of Dart related resource changes. * * @param delta the delta describing the resource changes (not {@code null}) */ public void traverse(IResourceDelta delta) throws CoreException { event = new Event(); processDelta(delta); event = null; } /** * Traverse the specified changes. * * @param delta the delta describing the resource changes (not {@code null}) */ protected void processDelta(IResourceDelta delta) throws CoreException { delta.accept(new IResourceDeltaVisitor() { @Override public boolean visit(IResourceDelta delta) throws CoreException { IResource resource = delta.getResource(); String name = resource.getName(); // Ignore hidden resources if (name.startsWith(".")) { return false; } if (resource.getType() == IResource.FILE) { // Notify listeners of changes to the pubspec.yaml file if (name.equals(PUBSPEC_FILE_NAME)) { event.setResource(resource, null, null); switch (delta.getKind()) { case ADDED: listener.pubspecAdded(event); break; case CHANGED: listener.pubspecChanged(event); break; case REMOVED: listener.pubspecRemoved(event); break; default: break; } return false; } // Notify context of Dart source that have been added, changed, or removed if (isDartLikeFileName(name) || isHtmlLikeFileName(name)) { event.setResource(resource, null, null); switch (delta.getKind()) { case ADDED: listener.sourceAdded(event); break; case CHANGED: listener.sourceChanged(event); break; case REMOVED: listener.sourceRemoved(event); break; default: break; } return false; } return false; } // Process "packages" folder changes separately if (name.equals(PACKAGES_DIRECTORY_NAME)) { boolean hasNewContext = setContextFor(resource.getParent()); if (pubFolder != null) { if (hasNewContext) { processPackagesDelta(delta); } return false; } } // Process folder changes switch (delta.getKind()) { case ADDED: processResources(resource); return false; case CHANGED: // Cache the context and traverse child resource changes return setContextFor((IContainer) resource); case REMOVED: if (setContextFor((IContainer) resource)) { event.setResource(resource, null, null); listener.sourceContainerRemoved(event); } return false; default: return false; } } }); } /** * Traverse changes in the "packages" directory * * @param delta the delta describing the resource changes (not {@code null}) */ protected void processPackagesDelta(IResourceDelta delta) throws CoreException { // Only process the top level "packages" folder in any given context IResource packagesContainer = delta.getResource(); if (!isTopContainerInContext(packagesContainer.getParent())) { return; } switch (delta.getKind()) { // If the "packages" directory is added, then traverse each package individually // because the canonical locations of each package differ case ADDED: for (IResource child : ((IContainer) packagesContainer).members()) { try { processPackagesResources( child, child.getLocation().toFile().getCanonicalFile(), child.getFullPath()); } catch (IOException e) { DartCore.logError(e); } } return; // Fall through to traverse child resource changes case CHANGED: break; // If all packages have been removed, then we issue a more general "packages removed" event // because the symlinks have been removed and thus the canonical locations lost case REMOVED: if (event.setPackagesRemoved()) { listener.packageSourceContainerRemoved(event); } return; default: return; } // Process each package delta in the "packages" directory for (IResourceDelta childDelta : delta.getAffectedChildren()) { // If the package has been removed, then we issue a more general "packages removed" event // because the symlink has been removed and thus the canonical location lost if (childDelta.getKind() == IResourceDelta.REMOVED) { IResource resource = childDelta.getResource(); String name = resource.getName(); // Ignore hidden resources and nested packages directories if (name.startsWith(".") || name.equals(PACKAGES_DIRECTORY_NAME)) { continue; } if (event.setPackagesRemoved()) { listener.packageSourceContainerRemoved(event); } continue; } // Determine the canonical location of the package IPath packageLocation = childDelta.getResource().getLocation(); if (packageLocation == null) { logNoLocation(childDelta.getResource()); continue; } File packageDir; try { packageDir = packageLocation.toFile().getCanonicalFile(); } catch (IOException e) { DartCore.logError(e); continue; } final File canonicalPackageDir = packageDir; final IPath fullPackagePath = childDelta.getResource().getFullPath(); // Traverse the package additions and changes childDelta.accept(new IResourceDeltaVisitor() { @Override public boolean visit(IResourceDelta delta) throws CoreException { IResource resource = delta.getResource(); String name = resource.getName(); // Ignore hidden resources and nested packages directories if (name.startsWith(".") || name.equals(PACKAGES_DIRECTORY_NAME)) { return false; } // Notify context of any Dart source files that have been added, changed, or removed if (resource.getType() == IResource.FILE) { if (isDartLikeFileName(name)) { event.setResource(resource, canonicalPackageDir, fullPackagePath); switch (delta.getKind()) { case ADDED: listener.packageSourceAdded(event); break; case CHANGED: listener.packageSourceChanged(event); break; case REMOVED: listener.packageSourceRemoved(event); break; default: break; } } return false; } // Notify context of any package folders that were added or removed switch (delta.getKind()) { case ADDED: processPackagesResources(resource, canonicalPackageDir, fullPackagePath); return false; case CHANGED: // Recursively visit changed resources return true; case REMOVED: event.setResource(resource, canonicalPackageDir, fullPackagePath); listener.packageSourceContainerRemoved(event); return false; default: return false; } } }); }; } /** * Traverse added resources in the "packages" directory * * @param resource the added resource (not {@code null}) * @param canonicalPackageDir the canonical package directory (not {@code null}) * @param fullPackagePath the full eclipse resource path of the package ( * {@link IResource#getFullPath()} not {@code null}) */ protected void processPackagesResources(IResource resource, final File canonicalPackageDir, final IPath fullPackagePath) throws CoreException { resource.accept(new IResourceProxyVisitor() { @Override public boolean visit(IResourceProxy proxy) throws CoreException { return visitPackagesProxy(proxy, proxy.getName(), canonicalPackageDir, fullPackagePath); } }, 0); } /** * Traverse the specified resources * * @param resource the resources to be recursively traversed */ protected void processResources(IResource resource) throws CoreException { if (!resource.exists()) { return; } resource.accept(new IResourceProxyVisitor() { @Override public boolean visit(IResourceProxy proxy) throws CoreException { return visitProxy(proxy, proxy.getName()); } }, 0); } /** * Visit a resource proxy in the "packages" directory. * * @param proxy the resource proxy (not {@code null}) * @param name the resource name (not {@code null}) * @param canonicalPackageDir the canonical package directory (not {@code null}) * @param fullPackagePath the full eclipse resource path of the package ( * {@link IResource#getFullPath()} not {@code null}) */ protected boolean visitPackagesProxy(IResourceProxy proxy, String name, File canonicalPackageDir, IPath fullPackagePath) { // Ignore hidden resources and nested packages directories if (name.startsWith(".") || name.equals(PACKAGES_DIRECTORY_NAME)) { return false; } // Notify context of any Dart source files that have been added if (proxy.getType() == FILE) { if (isDartLikeFileName(name)) { event.setProxy(proxy, canonicalPackageDir, fullPackagePath); listener.packageSourceAdded(event); } return false; } // Recursively visit nested folders return true; } /** * Visit a resource proxy. * * @param proxy the resource proxy (not {@code null}) * @param name the resource name (not {@code null}) */ protected boolean visitProxy(IResourceProxy proxy, String name) { // Ignore hidden resources and nested packages directories if (name.startsWith(".") || name.equals(PACKAGES_DIRECTORY_NAME)) { return false; } if (proxy.getType() == FILE) { // Notify listener of new pubspec.yaml files if (name.equals(PUBSPEC_FILE_NAME)) { event.setProxy(proxy, null, null); listener.pubspecAdded(event); return false; } // Notify listener of new source files if (isDartLikeFileName(name) || isHtmlLikeFileName(name)) { event.setProxy(proxy, null, null); listener.sourceAdded(event); return false; } return false; } // Cache the context and traverse child resource changes return setContextFor((IContainer) proxy.requestResource()); } /** * Return {@code true} if this container is a project or contains a pubspec.yaml */ private boolean isTopContainerInContext(IContainer container) { if (container.getType() == PROJECT) { return true; } AnalysisContext context = project.getContext(container); AnalysisContext parentContext = project.getContext(container.getParent()); return context != parentContext; } private void logNoLocation(IResource resource) { DartCore.logInformation("No location for " + resource); } /** * Cache the context for the specified container for use in subsequent operations. If the context * for the specified container cannot be determined, then the cached context is left unchanged. * * @param container the container (not {@code null}) * @return {@code true} if the context was set, or {@code false} if not */ private boolean setContextFor(IContainer container) { PubFolder newPubFolder = project.getPubFolder(container); if (pubFolder == null || pubFolder != newPubFolder) { pubFolder = newPubFolder; if (pubFolder != null) { if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { contextId = pubFolder.getContextId(); } else { context = pubFolder.getContext(); } event.setResource(pubFolder.getResource(), null, null); } else { if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { contextId = project.getDefaultContextId(); } else { context = project.getDefaultContext(); } event.setResource(project.getResource(), null, null); } listener.visitContext(event); } return true; } }