/******************************************************************************* * Copyright (c) 2007 IBM Corporation. * 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: * Robert Fuhrer (rfuhrer@watson.ibm.com) - initial API and implementation *******************************************************************************/ package org.eclipse.imp.builder; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; 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.IWorkspaceRunnable; import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.imp.preferences.IPreferencesService; import org.eclipse.imp.preferences.PreferenceConstants; import org.eclipse.imp.preferences.PreferencesService; import org.eclipse.imp.runtime.PluginBase; import org.eclipse.imp.runtime.RuntimePlugin; import org.eclipse.imp.utils.UnimplementedError; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.console.ConsolePlugin; import org.eclipse.ui.console.IConsole; import org.eclipse.ui.console.IConsoleManager; import org.eclipse.ui.console.MessageConsole; import org.eclipse.ui.console.MessageConsoleStream; public abstract class BuilderBase extends IncrementalProjectBuilder { /** * The name of the console used for any builders that don't provide an override * of getConsoleName() to use their own unique console */ public static final String IMP_BUILDER_CONSOLE= "IMP Builders"; /** * @return the plugin associated with this builder instance */ protected abstract PluginBase getPlugin(); /** * @return the extension ID of this builder */ public String getBuilderID() { throw new UnimplementedError("Not implemented for builder for plug-in " + getPlugin().getID()); } /** * @return true iff the given file is a source file that this builder should compile. */ protected abstract boolean isSourceFile(IFile file); /** * @return true iff the given file is a source file that this builder should scan * for dependencies, but not compile as a top-level compilation unit.<br> * <code>isNonRootSourceFile()</code> and <code>isSourceFile()</code> should never * return true for the same file. */ protected abstract boolean isNonRootSourceFile(IFile file); /** * @return true iff the given resource is an output folder */ protected abstract boolean isOutputFolder(IResource resource); /** * Does whatever is necessary to "compile" the given "source file". * @param file the "source file" to compile * @param monitor used to indicate progress in the UI */ protected abstract void compile(IFile file, IProgressMonitor monitor); /** * Collects compilation-unit dependencies for the given file, and records * them via calls to <code>fDependency.addDependency()</code>. */ protected abstract void collectDependencies(IFile file); /** * @return the ID of the marker type to be used to indicate compiler errors */ protected abstract String getErrorMarkerID(); /** * @return the ID of the marker type to be used to indicate compiler warnings */ protected abstract String getWarningMarkerID(); /** * @return the ID of the marker type to be used to indicate compiler information * messages */ protected abstract String getInfoMarkerID(); private final IResourceVisitor fResourceVisitor= new SourceCollectorVisitor(); private final IResourceDeltaVisitor fDeltaVisitor= new SourceDeltaVisitor(); private IPreferencesService fPrefService; protected DependencyInfo fDependencyInfo; private final Collection<IFile> fChangedSources= new HashSet<IFile>(); private final Collection<IFile> fSourcesToCompile= new HashSet<IFile>(); private final Collection<IFile> fSourcesForDeps= new HashSet<IFile>(); private final class SourceDeltaVisitor implements IResourceDeltaVisitor { public boolean visit(IResourceDelta delta) throws CoreException { return processResource(delta.getResource()); } } private class SourceCollectorVisitor implements IResourceVisitor { public boolean visit(IResource res) throws CoreException { return processResource(res); } } private boolean processResource(IResource resource) { if (resource instanceof IFile) { IFile file= (IFile) resource; if (file.exists()) { if (isSourceFile(file) || isNonRootSourceFile(file)) { fChangedSources.add(file); } } return false; } else if (isOutputFolder(resource)) { return false; } return true; } private class AllSourcesVisitor implements IResourceVisitor { private final Collection<IFile> fResult; public AllSourcesVisitor(Collection<IFile> result) { fResult= result; } public boolean visit(IResource resource) throws CoreException { if (resource instanceof IFile) { IFile file= (IFile) resource; if (file.exists()) { if (isSourceFile(file) || isNonRootSourceFile(file)) { fResult.add(file); } } return false; } else if (isOutputFolder(resource)) { return false; } return true; } } protected DependencyInfo createDependencyInfo(IProject project) { return new DependencyInfo(project); } @SuppressWarnings("unchecked") protected IProject[] build(int kind, Map args, IProgressMonitor monitor) { if (getPreferencesService().getProject() == null) { getPreferencesService().setProject(getProject()); } fChangedSources.clear(); fSourcesForDeps.clear(); fSourcesToCompile.clear(); boolean partialDeps= true; Collection<IFile> allSources= new ArrayList<IFile>(); if (fDependencyInfo == null || kind == FULL_BUILD || kind == CLEAN_BUILD) { fDependencyInfo= createDependencyInfo(getProject()); try { getProject().accept(new AllSourcesVisitor(allSources)); } catch (CoreException e) { getPlugin().getLog().log(new Status(IStatus.ERROR, getPlugin().getID(), e.getLocalizedMessage(), e)); } fSourcesForDeps.addAll(allSources); // Collect deps now, so we can compile everything necessary in the case where // we have no dep info yet (e.g. first build for this Eclipse invocation -- // we don't persist the dep info yet) but we've been asked to do an auto build // b/c of workspace changes. collectDependencies(monitor); partialDeps= false; } if (kind == FULL_BUILD || kind == CLEAN_BUILD) { clearMarkersOn(allSources); } try { collectSourcesToCompile(monitor); clearDependencyInfoForChangedFiles(); if (partialDeps) { fSourcesForDeps.addAll(fSourcesToCompile); fSourcesForDeps.addAll(fChangedSources); collectDependencies(monitor); } compileNecessarySources(monitor); if (getDiagPreference()) { getConsoleStream().print(fDependencyInfo.toString()); } } catch (CoreException e) { getPlugin().writeErrorMsg("Build failed: " + e.getMessage()); } return new IProject[0]; } protected void compileNecessarySources(IProgressMonitor monitor) { for(Iterator<IFile> iter= fSourcesToCompile.iterator(); iter.hasNext(); ) { IFile srcFile= iter.next(); clearMarkersOn(srcFile); if (isSourceFile(srcFile)) { compile(srcFile, monitor); } } } protected void collectDependencies(IProgressMonitor monitor) { for(IFile srcFile: fSourcesForDeps) { collectDependencies(srcFile); } } /** * Clears all problem markers (all markers whose type derives from IMarker.PROBLEM) * from the given file. A utility method for the use of derived builder classes. */ protected void clearMarkersOn(IFile file) { try { // SMS 28 Mar 2007 // Clear the markers for this builder only (and clear all of them) // (may be a simpler way to do this, given a more complex set up of // marker types) //file.deleteMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE); file.deleteMarkers(getErrorMarkerID(), true, IResource.DEPTH_INFINITE); file.deleteMarkers(getWarningMarkerID(), true, IResource.DEPTH_INFINITE); file.deleteMarkers(getInfoMarkerID(), true, IResource.DEPTH_INFINITE); } catch (CoreException e) { } } protected void clearMarkersOn(Collection<IFile> files) { for(IFile file: files) { clearMarkersOn(file); } } private void dumpSourceList(Collection<IFile> sourcesToCompile) { MessageConsoleStream consoleStream= getConsoleStream(); for(Iterator<IFile> iter= sourcesToCompile.iterator(); iter.hasNext(); ) { IFile srcFile= iter.next(); consoleStream.println(" " + srcFile.getFullPath()); } } /** * Clears the dependency information maintained for all files marked as * having changed (i.e. in <code>fSourcesToCompile</code>). */ private void clearDependencyInfoForChangedFiles() { for(Iterator<IFile> iter= fSourcesToCompile.iterator(); iter.hasNext(); ) { IFile srcFile= iter.next(); fDependencyInfo.clearDependenciesOf(srcFile.getFullPath().toString()); } } protected IPreferencesService getPreferencesService() { if (fPrefService == null) { fPrefService= new PreferencesService(null, getPlugin().getLanguageID()); } return fPrefService; } protected boolean getDiagPreference() { final IPreferencesService builderPrefSvc= getPlugin().getPreferencesService(); final IPreferencesService impPrefSvc= RuntimePlugin.getInstance().getPreferencesService(); boolean msgs= builderPrefSvc.isDefined(PreferenceConstants.P_EMIT_BUILDER_DIAGNOSTICS) ? builderPrefSvc.getBooleanPreference(PreferenceConstants.P_EMIT_BUILDER_DIAGNOSTICS) : impPrefSvc.getBooleanPreference(PreferenceConstants.P_EMIT_BUILDER_DIAGNOSTICS); return msgs; } /** * Visits the project delta, if any, or the entire project, and determines the set * of files needed recompilation, and adds them to <code>fSourcesToCompile</code>. * @param monitor * @throws CoreException */ private void collectSourcesToCompile(IProgressMonitor monitor) throws CoreException { IResourceDelta delta= getDelta(getProject()); boolean emitDiags= getDiagPreference(); if (delta != null) { if (emitDiags) getConsoleStream().println("==> Scanning resource delta for project '" + getProject().getName() + "'... <=="); delta.accept(fDeltaVisitor); if (emitDiags) getConsoleStream().println("Delta scan completed for project '" + getProject().getName() + "'..."); } else { if (emitDiags) getConsoleStream().println("==> Scanning for source files in project '" + getProject().getName() + "'... <=="); getProject().accept(fResourceVisitor); if (emitDiags) getConsoleStream().println("Source file scan completed for project '" + getProject().getName() + "'..."); } collectChangeDependents(); if (emitDiags) { getConsoleStream().println("All files to compile:"); dumpSourceList(fSourcesToCompile); } } // TODO This really *shouldn't* be transitive; the real problem w/ the LPGBuilder is that it // doesn't account for transitive includes itself when computing its dependency info. That is, // when file A includes B includes C, A should be marked as a dependent of C. private void collectChangeDependents() { if (fChangedSources.size() == 0) return; Collection<IFile> changeDependents= new HashSet<IFile>(); boolean emitDiags= getDiagPreference(); changeDependents.addAll(fChangedSources); if (emitDiags) { getConsoleStream().println("Changed files:"); dumpSourceList(changeDependents); } boolean changed= false; do { Collection<IFile> additions= new HashSet<IFile>(); scanSourceList(changeDependents, additions); changed= changeDependents.addAll(additions); } while (changed); for(IFile f: changeDependents) { if (isSourceFile(f)) { fSourcesToCompile.add(f); } } // getConsoleStream().println("Changed files + dependents:"); // dumpSourceList(fSourcesToCompile); } private boolean scanSourceList(Collection<IFile> srcList, Collection<IFile> changeDependents) { boolean result= false; for(Iterator<IFile> iter= srcList.iterator(); iter.hasNext(); ) { IFile srcFile= iter.next(); Set<String> fileDependents= fDependencyInfo.getDependentsOf(srcFile.getFullPath().toString()); if (fileDependents != null) { for(Iterator<String> iterator= fileDependents.iterator(); iterator.hasNext(); ) { String depPath= iterator.next(); IFile depFile= getProject().getWorkspace().getRoot().getFile(new Path(depPath)); result= result || changeDependents.add(depFile); } } } return result; } /** * Refreshes all resources in the entire project tree containing the given resource. * Crude but effective. */ protected void doRefresh(final IResource resource) { IWorkspaceRunnable r= new IWorkspaceRunnable() { public void run(IProgressMonitor monitor) throws CoreException { resource.getProject().refreshLocal(IResource.DEPTH_INFINITE, null); } }; try { getProject().getWorkspace().run(r, resource.getProject(), IWorkspace.AVOID_UPDATE, null); } catch (CoreException e) { getPlugin().logException("Error while refreshing after a build", e); } } /** * @return the ID of the marker type for the given marker severity (one of * <code>IMarker.SEVERITY_*</code>). If the severity is unknown/invalid, * returns <code>getInfoMarkerID()</code>. */ protected String getMarkerIDFor(int severity) { switch(severity) { case IMarker.SEVERITY_ERROR: return getErrorMarkerID(); case IMarker.SEVERITY_WARNING: return getWarningMarkerID(); case IMarker.SEVERITY_INFO: return getInfoMarkerID(); default: return getInfoMarkerID(); } } /** * Utility method to create a marker on the given resource using the given * information. * @param errorResource * @param startLine the line with which the error is associated * @param charStart the offset of the first character with which the error is associated * @param charEnd the offset of the last character with which the error is associated * @param message a human-readable text message to appear in the "Problems View" * @param severity the message severity, one of <code>IMarker.SEVERITY_*</code> */ public IMarker createMarker(IResource errorResource, int startLine, int charStart, int charEnd, String message, int severity) { try { // TODO: Address this situation properly after demo // Issue is resources that are templates and not in user's workspace if (!errorResource.exists()) return null; IMarker m = errorResource.createMarker(getMarkerIDFor(severity)); String[] attributeNames = new String[] {IMarker.LINE_NUMBER, IMarker.MESSAGE, IMarker.PRIORITY, IMarker.SEVERITY}; Object[] values = new Object[] {startLine, message, IMarker.PRIORITY_HIGH, severity}; m.setAttributes(attributeNames, values); if (charStart >= 0 && charEnd >= 0) { attributeNames = new String[] {IMarker.CHAR_START, IMarker.CHAR_END}; values = new Object[] {charStart, charEnd}; m.setAttributes(attributeNames, values); } else if (charStart >= 0) { m.setAttribute(IMarker.CHAR_START, charStart); } else if (charEnd >= 0) { m.setAttribute(IMarker.CHAR_END, charEnd); } return m; } catch (CoreException e) { getPlugin().writeErrorMsg("Unable to create marker: " + e.getMessage()); } return null; } /** * Posts a dialog displaying the given message as soon as "conveniently possible". * This is not a synchronous call, since this method will get called from a * different thread than the UI thread, which is the only thread that can * post the dialog box. */ protected void postMsgDialog(final String title, final String msg) { PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { public void run() { Shell shell= RuntimePlugin.getInstance().getWorkbench().getActiveWorkbenchWindow().getShell(); MessageDialog.openInformation(shell, title, msg); } }); } /** * Posts a dialog displaying the given message as soon as "conveniently possible". * This is not a synchronous call, since this method will get called from a * different thread than the UI thread, which is the only thread that can * post the dialog box. */ protected void postQuestionDialog(final String title, final String query, final Runnable runIfYes, final Runnable runIfNo) { PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { public void run() { Shell shell= RuntimePlugin.getInstance().getWorkbench().getActiveWorkbenchWindow().getShell(); boolean response= MessageDialog.openQuestion(shell, title, query); if (response) runIfYes.run(); else if (runIfNo != null) runIfNo.run(); } }); } /** * Derived classes may override to specify a unique name for a separate console; * otherwise, all IMP builders share a single console. @see IMP_BUILDER_CONSOLE. * @return the name of the console to use for diagnostic output, if any */ protected String getConsoleName() { return IMP_BUILDER_CONSOLE; } /** * If you want your builder to have its own console, be sure to override * getConsoleName(). * @return the console whose name is returned by getConsoleName() */ protected MessageConsoleStream getConsoleStream() { return findConsole(getConsoleName()).newMessageStream(); } /** * Find or create the console with the given name * @param consoleName */ protected MessageConsole findConsole(String consoleName) { if (consoleName == null) { RuntimePlugin.getInstance().getLog().log(new Status(IStatus.ERROR, RuntimePlugin.IMP_RUNTIME, "BuilderBase.findConsole() called with a null console name; substituting default console")); consoleName= IMP_BUILDER_CONSOLE; } MessageConsole myConsole= null; final IConsoleManager consoleManager= ConsolePlugin.getDefault().getConsoleManager(); IConsole[] consoles= consoleManager.getConsoles(); for(int i= 0; i < consoles.length; i++) { IConsole console= consoles[i]; if (console.getName().equals(consoleName)) myConsole= (MessageConsole) console; } if (myConsole == null) { myConsole= new MessageConsole(consoleName, null); consoleManager.addConsoles(new IConsole[] { myConsole }); } // consoleManager.showConsoleView(myConsole); return myConsole; } }