/***************************************************************************** * Copyright (c) 2006-2013, Cloudsmith Inc. * The code, documentation and other materials contained herein have been * licensed under the Eclipse Public License - v 1.0 by the copyright holder * listed above, as the Initial Contributor under such license. The text of * such license is available at www.eclipse.org. *****************************************************************************/ package org.eclipse.buckminster.core.metadata; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.buckminster.core.CorePlugin; import org.eclipse.buckminster.core.Messages; import org.eclipse.buckminster.core.cspec.AbstractResolutionBuilder; import org.eclipse.buckminster.core.cspec.ICSpecData; import org.eclipse.buckminster.core.cspec.IComponentRequest; import org.eclipse.buckminster.core.cspec.model.ComponentIdentifier; import org.eclipse.buckminster.core.cspec.model.ComponentRequest; import org.eclipse.buckminster.core.ctype.IComponentType; import org.eclipse.buckminster.core.helpers.FileUtils; import org.eclipse.buckminster.core.helpers.TextUtils; import org.eclipse.buckminster.core.metadata.model.Materialization; import org.eclipse.buckminster.core.metadata.model.Resolution; import org.eclipse.buckminster.core.query.builder.ComponentQueryBuilder; import org.eclipse.buckminster.core.reader.EclipsePreferencesReader; import org.eclipse.buckminster.core.resolver.LocalResolver; import org.eclipse.buckminster.core.resolver.ResolutionContext; import org.eclipse.buckminster.runtime.AttachableProgressMonitor; import org.eclipse.buckminster.runtime.BuckminsterException; import org.eclipse.buckminster.runtime.Logger; import org.eclipse.buckminster.runtime.MonitorUtils; import org.eclipse.core.internal.resources.ProjectDescription; 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.IResourceVisitor; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ProjectScope; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtensionRegistry; 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.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.osgi.util.NLS; @SuppressWarnings("restriction") public class MetadataSynchronizer implements IResourceChangeListener { class MetadataRefreshJob extends Job { public MetadataRefreshJob() { super(Messages.Metadata_refresh); setPriority(SHORT); setSystem(true); } @Override protected IStatus run(IProgressMonitor monitor) { int ticks; synchronized (MetadataSynchronizer.this) { ticks = removedEntries.size() * 30 + projectsNeedingUpdate.size() * 70; } if (ticks == 0) { MonitorUtils.complete(monitor); return Status.OK_STATUS; } monitor.beginTask(null, ticks); try { StorageManager sm = StorageManager.getDefault(); boolean didSomething = true; for (; didSomething && defaultSynchronizer != null;) { didSomething = false; IPath removedEntry; while (defaultSynchronizer != null && (removedEntry = getNextRemovedEntry()) != null) { didSomething = true; for (Materialization mat : sm.getMaterializations().getElements()) { if (mat.getComponentLocation().equals(removedEntry)) { // The project has been removed. This does't // mean that // we have to remove the materialization. For // that to // happen, the actual content must have been // removed // too. // if (!removedEntry.toFile().exists()) { // Try and remove the resolution. It might // not work // Resolution res = WorkspaceInfo.getResolution(mat.getComponentIdentifier()); try { res.remove(sm); } catch (ReferentialIntegrityException e) { } mat.remove(sm); } break; } } MonitorUtils.worked(monitor, 30); } IProject project; while (defaultSynchronizer != null && (project = getNextProjectNeedingUpdate()) != null) { didSomething = true; monitor.subTask(NLS.bind(Messages.Refreshing_0, project.getName())); try { refreshProject(project, MonitorUtils.subMonitor(monitor, 70)); } catch (Exception e) { if (defaultSynchronizer != null && project.isAccessible()) CorePlugin.getLogger().error(e, NLS.bind(Messages.Project_refresh_on_0_failed_1, project.getName(), e.getMessage())); } } } return Status.OK_STATUS; } catch (Exception e) { if (defaultSynchronizer != null) CorePlugin.getLogger().error(e, e.toString()); return BuckminsterException.wrap(e).getStatus(); } } } static class ResetVisitor implements IResourceVisitor { @Override public boolean visit(IResource resource) throws CoreException { if ((resource instanceof IProject) && !((IProject) resource).isOpen()) return false; String cidStr = resource.getPersistentProperty(WorkspaceInfo.PPKEY_COMPONENT_ID); if (cidStr != null) resource.setPersistentProperty(WorkspaceInfo.PPKEY_COMPONENT_ID, null); return true; } } static class WorkspaceCatchUpJob extends Job { private static final QualifiedName QN_ATTACHABLE_PROGRESS_MONITOR = new QualifiedName(CorePlugin.getID(), "attachableProgressMonitor"); //$NON-NLS-1$ private AttachableProgressMonitor attachableMonitor; public WorkspaceCatchUpJob() { super(Messages.Buckminster_workspace_catch_up); // We need very high prio on this since we wait // for it to complete during plug-in activation // setPriority(Job.SHORT); attachableMonitor = new AttachableProgressMonitor(); setProperty(QN_ATTACHABLE_PROGRESS_MONITOR, attachableMonitor); } @Override public boolean belongsTo(Object family) { if (family == MetadataSynchronizer.class) return true; return false; } @Override protected IStatus run(IProgressMonitor monitor) { monitor = attachableMonitor.wrap(monitor); monitor.beginTask(Messages.Refreshing_project_meta_data, 1000); try { workspaceCatchUp(monitor); } catch (Exception e) { CorePlugin.getLogger().warning(e, NLS.bind(Messages.Problem_during_meta_data_refresh_0, e.getMessage())); } finally { setProperty(QN_ATTACHABLE_PROGRESS_MONITOR, null); monitor.done(); } return Status.OK_STATUS; } } private class Visitor implements IResourceDeltaVisitor { @Override public boolean visit(IResourceDelta delta) throws CoreException { int kind = delta.getKind(); IResource resource = delta.getResource(); if (kind == IResourceDelta.REMOVED) { if ((delta.getFlags() & IResourceDelta.MOVED_TO) != 0) return false; if (resource == null) return false; // Project is probably removed. If it is, we should have it in // our cache // IProject project = resource.getProject(); if (project == null) return true; IPath relPath = resource.getProjectRelativePath(); IPath path = resource.getLocation(); if (path == null) { IPath projPath = deletedProjectLocations.get(project.getName()); if (projPath == null) // // No location and not a remove project. We have no clue // what to do with this. // return false; path = projPath.append(relPath); } if (!(resource instanceof IFile)) path = path.addTrailingSeparator(); synchronized (MetadataSynchronizer.this) { removedEntries.add(path); if (isCSpecSource(resource, relPath)) { projectsNeedingUpdate.add(resource.getProject()); return false; } return true; } } if (kind == IResourceDelta.ADDED && (delta.getFlags() & IResourceDelta.MOVED_FROM) != 0) { synchronized (MetadataSynchronizer.this) { WorkspaceInfo.setComponentIdentifier(resource, null); projectsNeedingUpdate.add(resource.getProject()); } return false; } if (kind == IResourceDelta.ADDED || (delta.getFlags() & (IResourceDelta.CONTENT | IResourceDelta.REPLACED)) != 0) { if (resource instanceof IProject || isCSpecSource(resource, resource.getProjectRelativePath())) { synchronized (MetadataSynchronizer.this) { projectsNeedingUpdate.add(resource.getProject()); } return false; } } return true; } } private static MetadataSynchronizer defaultSynchronizer = new MetadataSynchronizer(); public static MetadataSynchronizer getDefault() { return defaultSynchronizer; } public static void refreshProject(IProject project, IProgressMonitor monitor) throws CoreException { if (project.getName().equals(CorePlugin.BUCKMINSTER_PROJECT) || !project.isAccessible()) { MonitorUtils.complete(monitor); return; } IPath location = project.getLocation(); if (location == null) return; Resolution oldInfo = null; ComponentIdentifier cid = WorkspaceInfo.getComponentIdentifier(project); if (cid != null) oldInfo = WorkspaceInfo.getResolution(cid); monitor.beginTask(null, 100); try { ComponentRequest request; if (oldInfo == null) request = new ComponentRequest(project.getName(), null, null); else request = new ComponentRequest(oldInfo.getRequest().getName(), null, null); ComponentQueryBuilder queryBld = new ComponentQueryBuilder(); queryBld.setRootRequest(request); queryBld.setPlatformAgnostic(true); ResolutionContext context = new ResolutionContext(queryBld.createComponentQuery()); Resolution res = LocalResolver.fromPath(context.getRootNodeQuery(), project.getLocation(), oldInfo); if (!res.equals(oldInfo)) { StorageManager sm = StorageManager.getDefault(); res.store(sm); ComponentIdentifier ci = res.getComponentIdentifier(); Materialization mat = new Materialization(location.addTrailingSeparator(), ci); mat.store(sm); WorkspaceInfo.setComponentIdentifier(project, ci); if (oldInfo != null) { try { oldInfo.remove(sm); oldInfo.getCSpec().remove(sm); } catch (ReferentialIntegrityException e) { // Old resolution is being held by a BillOfMaterials. It // cannot be removed at this point. } } } updateProjectReferences(project, res.getCSpec(), MonitorUtils.subMonitor(monitor, 50)); } finally { monitor.done(); } } public static void setUp() { IExtensionRegistry exReg = Platform.getExtensionRegistry(); if (exReg == null) return; // We died before we got the chance IConfigurationElement[] elems = exReg.getConfigurationElementsFor(CorePlugin.COMPONENT_TYPE_POINT); // The extension file can exist in every project but we use Buckminster // component // type as a placeholder. // defaultSynchronizer.registerCSpecSource(CorePlugin.CSPECEXT_FILE); defaultSynchronizer.registerCSpecSource(EclipsePreferencesReader.BUCKMINSTER_PROJECT_PREFS_PATH); for (IConfigurationElement elem : elems) { for (IConfigurationElement metaFile : elem.getChildren("metaFile")) //$NON-NLS-1$ { String metaPath = metaFile.getAttribute("path"); //$NON-NLS-1$ if (metaPath == null) continue; metaPath = metaPath.trim(); if (metaPath.length() > 0) defaultSynchronizer.registerCSpecSource(metaPath); for (String alias : TextUtils.split(metaFile.getAttribute("aliases"), ",")) //$NON-NLS-1$ //$NON-NLS-2$ { alias = alias.trim(); if (alias.length() > 0) defaultSynchronizer.registerCSpecSource(alias); } } } IWorkspace ws = ResourcesPlugin.getWorkspace(); ws.addResourceChangeListener(defaultSynchronizer, IResourceChangeEvent.PRE_DELETE | IResourceChangeEvent.POST_CHANGE); } public static void tearDown() { MetadataSynchronizer mds = defaultSynchronizer; if (mds == null) return; defaultSynchronizer = null; IWorkspace ws = ResourcesPlugin.getWorkspace(); if (ws != null) ws.removeResourceChangeListener(mds); synchronized (mds) { if (mds.currentRefreshJob != null) { mds.currentRefreshJob.cancel(); try { // Give the job some time to cancel // Thread.sleep(500); } catch (InterruptedException e) { } } } } public static void workspaceCatchUp(IProgressMonitor monitor) throws CoreException { IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); wsRoot.accept(new ResetVisitor()); MonitorUtils.worked(monitor, 50); IProject[] projects = wsRoot.getProjects(); MonitorUtils.worked(monitor, 50); // Re-resolve all projects // if (projects.length > 0) { int ticksPerRefresh = 900 / projects.length; for (IProject project : projects) { try { refreshProject(project, MonitorUtils.subMonitor(monitor, ticksPerRefresh)); } catch (Exception e) { CorePlugin.getLogger().warning(e, NLS.bind(Messages.Problem_during_meta_data_refresh_0, e.getMessage())); } } } } private static void updateProjectReferences(IProject project, ICSpecData cspec, IProgressMonitor monitor) throws CoreException { Collection<? extends IComponentRequest> crefs = cspec.getDependencies(); if (crefs.size() == 0) { // No use continuing. Project doesn't have any references. // MonitorUtils.complete(monitor); return; } // Create a set containing the references that are already present // ProjectDescription projDesc = (ProjectDescription) project.getDescription(); IProject[] refs = projDesc.getAllReferences(false); HashSet<String> oldSet = new HashSet<String>(refs.length); for (IProject oldRef : refs) oldSet.add(oldRef.getName()); Logger logger = CorePlugin.getLogger(); monitor.beginTask(null, 50 + crefs.size() * 10); ArrayList<IProject> refdProjs = null; for (IComponentRequest cref : crefs) { for (IResource resource : WorkspaceInfo.getResources(cref)) { if (!(resource instanceof IProject)) // // Component is not a project. This is OK but the component // doesn't // qualify as a project dependency (it can't be built). // continue; IProject refdProj = (IProject) resource; if (!refdProj.isOpen()) { logger.warning(NLS.bind(Messages.Project_0_references_closed_project_1, project.getName(), cref.getName())); } else if (!oldSet.contains(refdProj.getName())) { // Didn't have this one. // if (refdProjs == null) { refdProjs = new ArrayList<IProject>(); for (IProject dynRef : projDesc.getDynamicReferences(false)) refdProjs.add(dynRef); } refdProjs.add(refdProj); } } monitor.worked(10); } if (refdProjs != null) { refs = refdProjs.toArray(new IProject[refdProjs.size()]); projDesc.setDynamicReferences(refs); project.setDescription(projDesc, MonitorUtils.subMonitor(monitor, 50)); if (logger.isDebugEnabled()) { StringBuilder bld = new StringBuilder(); bld.append(NLS.bind(Messages.Project_0_now_has_dynamic_dependencies_to, project.getName())); for (IProject ref : refs) { bld.append(' '); bld.append(ref.getName()); } logger.debug(bld.toString()); } } monitor.done(); } private final HashMap<String, Pattern> cspecSources = new HashMap<String, Pattern>(); private MetadataRefreshJob currentRefreshJob; private final HashMap<String, IPath> deletedProjectLocations = new HashMap<String, IPath>(); private final Set<IProject> projectsNeedingUpdate = new HashSet<IProject>(); private final Set<IPath> removedEntries = new HashSet<IPath>(); public void registerCSpecSource(String path) { path = TextUtils.notEmptyTrimmedString(path); if (path == null) return; StringBuilder bld = new StringBuilder(path.length() + 10); bld.append('^'); int top = path.length(); for (int idx = 0; idx < top; ++idx) { char c = path.charAt(idx); switch (c) { case '\\': bld.append('/'); // We compare with path.toPortableString() break; case '.': case '{': case '}': case '[': case ']': case '+': case '^': case '$': case '(': case ')': case '|': case '&': bld.append('\\'); bld.append(c); break; case '?': bld.append('.'); break; case '*': bld.append(".*"); //$NON-NLS-1$ break; default: bld.append(c); } } bld.append('$'); int flags = 0; if (FileUtils.CASE_INSENSITIVE_FS) flags = Pattern.CASE_INSENSITIVE; cspecSources.put(path, Pattern.compile(bld.toString(), flags)); } @Override public void resourceChanged(IResourceChangeEvent event) { if (defaultSynchronizer == null) // // We're shutting down so never mind. // return; if (event.getType() == IResourceChangeEvent.PRE_DELETE) { IResource resource = event.getResource(); if (resource instanceof IProject) deletedProjectLocations.put(resource.getName(), resource.getLocation()); return; } try { event.getDelta().accept(new Visitor()); deletedProjectLocations.clear(); } catch (CoreException e) { CorePlugin.getLogger().error(e, e.getMessage()); } synchronized (this) { if (currentRefreshJob == null && (projectsNeedingUpdate.size() > 0 || removedEntries.size() > 0)) { // Start a refresh job. // currentRefreshJob = new MetadataRefreshJob(); currentRefreshJob.addJobChangeListener(new JobChangeAdapter() { @Override public void done(IJobChangeEvent ev) { // I'm about to terminate. First make absolutely sure // that there's nothing // left to do // if (defaultSynchronizer == null) // // We're shutting down // return; synchronized (MetadataSynchronizer.this) { if (removedEntries.isEmpty() && projectsNeedingUpdate.isEmpty()) { // We're done // currentRefreshJob = null; } else currentRefreshJob.schedule(); } } }); currentRefreshJob.schedule(); } } } synchronized IProject getNextProjectNeedingUpdate() { if (projectsNeedingUpdate.isEmpty()) return null; IProject entry = projectsNeedingUpdate.iterator().next(); projectsNeedingUpdate.remove(entry); return entry; } synchronized IPath getNextRemovedEntry() { if (removedEntries.isEmpty()) return null; IPath entry = removedEntries.iterator().next(); removedEntries.remove(entry); return entry; } private boolean isCSpecSource(IResource resource, IPath path) { String pathStr = path.toPortableString(); for (Pattern pattern : cspecSources.values()) { Matcher m = pattern.matcher(pathStr); if (m.matches()) return true; } IProject project = resource.getProject(); if (project == null) return false; ProjectScope scope = new ProjectScope(project); IEclipsePreferences prefs = scope.getNode(CorePlugin.getID()); String tmp = AbstractResolutionBuilder.getMetadataFile(prefs, IComponentType.PREF_CSPEC_FILE, CorePlugin.CSPEC_FILE); if (path.equals(Path.fromPortableString(tmp))) return true; tmp = AbstractResolutionBuilder.getMetadataFile(prefs, IComponentType.PREF_CSPEX_FILE, CorePlugin.CSPECEXT_FILE); return path.equals(Path.fromPortableString(tmp)); } }