package com.anjlab.eclipse.tapestry5.watchdog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
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.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaModelMarker;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import com.anjlab.eclipse.tapestry5.Activator;
import com.anjlab.eclipse.tapestry5.EclipseUtils;
import com.anjlab.eclipse.tapestry5.LibraryMapping;
import com.anjlab.eclipse.tapestry5.TapestryContext;
import com.anjlab.eclipse.tapestry5.TapestryFile;
import com.anjlab.eclipse.tapestry5.TapestryModule;
import com.anjlab.eclipse.tapestry5.TapestryProject;
import com.anjlab.eclipse.tapestry5.TapestryUtils;
public class TapestryProjectWatchdog extends AbstractWatchdog
{
private final class TapestryProjectAnalyzerJob extends Job
{
public static final String FAMILY_NAME = EclipseUtils.ECLIPSE_INTEGRATION_FOR_TAPESTRY5;
private final IWorkbenchWindow window;
private final IProject project;
private TapestryProjectAnalyzerJob(IWorkbenchWindow window, IProject project)
{
super(EclipseUtils.ECLIPSE_INTEGRATION_FOR_TAPESTRY5);
this.window = window;
this.project = project;
// Don't show progress pop-up to the user's face
setUser(false);
// Low priority
setPriority(DECORATE);
}
@Override
protected IStatus run(IProgressMonitor monitor)
{
try
{
waitForOtherJobs(monitor);
}
catch (OperationCanceledException e)
{
return Status.CANCEL_STATUS;
}
catch (InterruptedException e)
{
return Status.CANCEL_STATUS;
}
// TODO Check if there's any build errors in this project
// Analyzing broken project is extremely slow
monitor.beginTask("Analyzing " + project.getName(), IProgressMonitor.UNKNOWN);
monitor.worked(1);
final TapestryProject newTapestryProject = new TapestryProject(project);
newTapestryProject.initialize(monitor);
if (monitor.isCanceled())
{
// Don't propagate project changes if the job was canceled
return Status.CANCEL_STATUS;
}
EclipseUtils.asyncExec(window.getShell(), new Runnable()
{
@Override
public void run()
{
currentProjects.put(window, newTapestryProject);
notifyProjectChanged(window, newTapestryProject);
}
});
return Status.OK_STATUS;
}
private void waitForOtherJobs(IProgressMonitor monitor) throws OperationCanceledException, InterruptedException
{
getJobManager().join(ResourcesPlugin.FAMILY_AUTO_BUILD, monitor);
getJobManager().join(ResourcesPlugin.FAMILY_MANUAL_BUILD, monitor);
getJobManager().join(JavaUI.ID_PLUGIN, monitor);
}
@Override
public boolean belongsTo(Object family)
{
return FAMILY_NAME.equals(family);
}
}
private static void cancelOtherJobsOfThisKind(IWorkbenchWindow window)
{
Job[] jobs = Job.getJobManager().find(TapestryProjectAnalyzerJob.FAMILY_NAME);
for (Job job : jobs)
{
if (job instanceof TapestryProjectAnalyzerJob)
{
if (((TapestryProjectAnalyzerJob) job).window == window)
{
job.cancel();
}
}
}
}
private WindowSelectionListener windowListener;
private final Map<IWorkbenchWindow, TapestryProject> currentProjects;
private IResourceChangeListener postChangeListener;
public TapestryProjectWatchdog()
{
currentProjects = new HashMap<IWorkbenchWindow, TapestryProject>();
}
@Override
public void start()
{
super.start();
windowListener = new WindowSelectionListener(new ISelectionListener()
{
private final ActiveEditorTracker activeEditorTracker = new ActiveEditorTracker();
@Override
public void selectionChanged(IWorkbenchPart part, ISelection selection)
{
IWorkbenchPage page = part.getSite().getPage();
// https://github.com/anjlab/eclipse-tapestry5-plugin/issues/18
if (!activeEditorTracker.editorChanged(page))
{
return;
}
TapestryFile tapestryFile = TapestryUtils.getTapestryFileFromPage(page);
final IProject project = tapestryFile != null
? tapestryFile.getProject()
: EclipseUtils.getProjectFromSelection(selection);
if (project == null)
{
return;
}
final IWorkbenchWindow window = part.getSite().getWorkbenchWindow();
TapestryProject tapestryProject = currentProjects.get(window);
if (tapestryProject == null
|| (!tapestryProject.contains(project)
&& TapestryUtils.isTapestryProject(project)))
{
changeTapestryProject(window, project);
}
}
})
.addListener();
postChangeListener = new IResourceChangeListener()
{
@Override
public void resourceChanged(IResourceChangeEvent event)
{
Set<IProject> affectedProjects = new HashSet<IProject>();
List<IFile> affectedFiles = EclipseUtils.getAllAffectedResources(event.getDelta(), IFile.class);
// XXX This event is triggered twice on clean & build
filterOutFilesLocatedInOutputFolders(affectedFiles);
if (affectedFiles.size() == 0)
{
return;
}
if (affectedFiles.size() > 25)
{
// Probably build is going on. It's faster to re-analyze entire project,
// because number of files we should check may be huge here
affectedFiles.clear();
affectedProjects.addAll(EclipseUtils.getAllAffectedResources(event.getDelta(), IProject.class));
}
for (IFile affectedFile : affectedFiles)
{
// Some project files might have been changed:
//
// 1) Classpath => SubModules added/removed
// 2) web.xml => AppModule may have been reconfigured as well as list of Development/QA/Production modules
// 3) Module's Java file => SubModules, LibraryMappings changed
// 4) Tapestry Context => Removed/Added, Component Specification changed
if (isEclipseProjectClasspathFile(affectedFile))
{
// We don't want to construct tapestry project multiple times if there's another file from this project that is changed
// Collect all affected eclipse projects first, then update corresponding tapestry projects if needed
affectedProjects.add(affectedFile.getProject());
continue;
}
if (isWebXmlFile(affectedFile))
{
affectedProjects.add(affectedFile.getProject());
continue;
}
if (affectedFile.getName().endsWith(".class"))
{
continue;
}
TapestryContext context = Activator.getDefault()
.getTapestryContextFactory()
.createTapestryContext(affectedFile);
for (Entry<IWorkbenchWindow, TapestryProject> entry : currentProjects.entrySet())
{
TapestryProject project = entry.getValue();
for (TapestryModule module : project.modules())
{
if (module.isReadOnly() && !module.isTapestryCoreModule())
{
// We don't expect that files from read only modules will be changed
continue;
}
if (TapestryUtils.isModuleFile(affectedFile, module))
{
Activator.getDefault().getTapestryModuleFactory().localModuleChanged(affectedFile);
affectedProjects.add(affectedFile.getProject());
continue;
}
for (LibraryMapping mapping : module.libraryMappings())
{
// TODO Support pages and mixins
if (context.getPackageName() != null
&& context.getPackageName().startsWith(
mapping.getRootPackage() + ".components"))
{
TapestryModule targetModule = module;
// XXX DRY: see TapestryModule#findComponents
if (mapping.getPathPrefix().isEmpty() && module.isTapestryCoreModule())
{
// This package is from the AppModule
for (TapestryModule m : module.getProject().modules())
{
if (m.isAppModule())
{
targetModule = m;
break;
}
}
}
List<TapestryContext> components = targetModule.getComponents();
boolean replaced = false;
for (int i = 0; i < components.size(); i++)
{
TapestryContext component = components.get(i);
if (StringUtils.equals(component.getPackageName() + "." + component.getName(),
context.getPackageName() + "." + context.getName()))
{
if (context.getInitialFile().exists())
{
// Replace with new context
components.set(i, context);
}
else
{
// File deleted
components.remove(i--);
}
replaced = true;
break;
}
}
if (!replaced)
{
// New component
components.add(context);
}
}
}
}
}
}
// Find out which tapestry projects should be re-analyzed
// TODO This should better be done after project build
Set<IProject> updatedProjects = new HashSet<IProject>();
for (Entry<IWorkbenchWindow, TapestryProject> entry : currentProjects.entrySet())
{
TapestryProject tapestryProject = entry.getValue();
for (IProject project : affectedProjects)
{
if (tapestryProject.contains(project))
{
if (!updatedProjects.contains(tapestryProject.getProject()))
{
if (hasProblems(tapestryProject.getProject()))
{
// Java search is very slow on broken
// projects, skip automatic refresh
// TODO Update icon in Tapestry Project
// Outline to indicate it's probably out of
// sync
continue;
}
updateProject(tapestryProject.getProject());
updatedProjects.add(tapestryProject.getProject());
}
}
}
}
// Usually updatedProjects will be empty here, because web.xml,
// classpath and Tapestry modules are not changed very often,
// but contexts do. And it is not necessary to re-analyze entire tapestry project,
// simply updating this context in corresponding module should be enough
// XXX Project analysis is a background job which may be still running now,
// so we should wait for it before updating corresponding module
// Though if some tapestry project was re-analyzed because of affected changes, then
// it should have been also pick up all the changes of its contexts
// XXX How can we know that this context is from the project that is now under analysis?
// Some modules might have been changed (added/removed) -- Update modules before analysis?
}
private void filterOutFilesLocatedInOutputFolders(List<IFile> affectedFiles)
{
Map<IProject, List<IPath>> outputLocations = new HashMap<IProject, List<IPath>>();
for (int i = 0; i < affectedFiles.size(); i++)
{
IFile affectedFile = affectedFiles.get(i);
IProject project = affectedFile.getProject();
List<IPath> locations = outputLocations.get(project);
if (locations == null)
{
locations = findOutputLocations(project);
outputLocations.put(project, locations);
}
for (IPath output : locations)
{
if (output.isPrefixOf(affectedFile.getFullPath()))
{
// This file is from the output folder, ignore it
// XXX ArrayList isn't the best choice for mutable lists
affectedFiles.remove(i--);
break;
}
}
}
}
private List<IPath> findOutputLocations(IProject project)
{
List<IPath> locations = new ArrayList<IPath>();
try
{
if (EclipseUtils.isJavaProject(project))
{
IJavaProject javaProject = JavaCore.create(project);
if (javaProject.getOutputLocation() != null)
{
locations.add(javaProject.getOutputLocation());
}
for (IPackageFragmentRoot root : javaProject.getPackageFragmentRoots())
{
if (root instanceof IClasspathEntry)
{
IPath outputLocation = ((IClasspathEntry) root).getOutputLocation();
if (outputLocation != null)
{
locations.add(outputLocation);
}
}
}
}
}
catch (CoreException e)
{
// Ignore
}
return locations;
}
private boolean isWebXmlFile(IFile affectedFile)
{
return affectedFile.getName().equals("web.xml")
&& ObjectUtils.equals(affectedFile, TapestryUtils.findWebXml(affectedFile.getProject()));
}
private boolean isEclipseProjectClasspathFile(IFile affectedFile)
{
return affectedFile.getName().equals(".classpath")
&& ObjectUtils.equals(affectedFile.getParent(), affectedFile.getProject());
}
};
ResourcesPlugin.getWorkspace().addResourceChangeListener(postChangeListener, IResourceChangeEvent.POST_CHANGE);
}
private void updateProject(IProject affectedProject)
{
// Check if there's any window that has this project as current
for (Entry<IWorkbenchWindow, TapestryProject> entry : currentProjects.entrySet())
{
IWorkbenchWindow window = entry.getKey();
TapestryProject project = entry.getValue();
for (TapestryModule module : project.modules())
{
if (ObjectUtils.equals(affectedProject, module.getEclipseProject()))
{
// Let every window get its own copy of TapestryProject
changeTapestryProject(window, affectedProject);
break;
}
}
}
}
private synchronized void changeTapestryProject(final IWorkbenchWindow window, final IProject project)
{
// Other jobs may still be running, cancel them and don't even schedule new job until they completed
cancelOtherJobsOfThisKind(window);
try
{
Job.getJobManager().join(TapestryProjectAnalyzerJob.FAMILY_NAME, new NullProgressMonitor());
}
catch (OperationCanceledException e)
{
// Ignore
}
catch (InterruptedException e)
{
// Ignore
}
Job analyzeProject = new TapestryProjectAnalyzerJob(window, project);
analyzeProject.schedule();
}
private void notifyProjectChanged(IWorkbenchWindow targetWindow, TapestryProject newTapestryProject)
{
for (ITapestryContextListener listener : listeners.find(ITapestryContextListener.class, targetWindow, true))
{
listener.projectChanged(targetWindow, newTapestryProject);
}
}
@Override
public void stop()
{
windowListener.removeListener();
windowListener = null;
ResourcesPlugin.getWorkspace().removeResourceChangeListener(postChangeListener);
postChangeListener = null;
super.stop();
}
public TapestryProject getTapestryProject(IWorkbenchWindow window)
{
return currentProjects.get(window);
}
public void forceProjectRefresh(IWorkbenchWindow window)
{
TapestryProject tapestryProject = currentProjects.get(window);
if (tapestryProject != null)
{
IProject project = tapestryProject.getProject();
Activator.getDefault().getTapestryModuleFactory().clearCache(project);
updateProject(project);
}
}
private static boolean hasProblems(IProject project)
{
try
{
IMarker[] markers = project.findMarkers(
IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER,
true,
IResource.DEPTH_INFINITE);
for (IMarker marker : markers)
{
Integer severityType = (Integer) marker.getAttribute(IMarker.SEVERITY);
if (severityType.intValue() == IMarker.SEVERITY_ERROR)
{
return true;
}
}
return false;
}
catch (CoreException e)
{
Activator.getDefault().logError("Error determining project problems", e);
return true;
}
}
}