/*******************************************************************************
* Copyright (C) 2003, 2006-2008, 2013, Guillaume Brocker
* Copyright (C) 2015-2017, Andre Bossert
*
* 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:
* Guillaume Brocker - Initial API and implementation
* Andre Bossert - improved thread handling, added show command in console / title
* - Add ability to use Doxyfile not in project scope
* - Refactoring of deprecated API usage
* - Support resources in linked folders
* https://github.com/anb0s/eclox/issues/176
* Thanks to Corderbollie!
* - fixed java.lang.IllegalArgumentException: endRule without matching beginRule
* https://github.com/anb0s/eclox/issues/175
* - fixed obsolete settings are not marked
* https://github.com/anb0s/eclox/issues/187
*
******************************************************************************/
package eclox.core.doxygen;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
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.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.filesystem.URIUtil;
import eclox.core.Plugin;
import eclox.core.doxyfiles.Doxyfile;
/**
* Implement a build job.
*
* @author Guillaume Brocker
*/
public class BuildJob extends Job {
/**
* Defines the doxygen not found error code
*/
public static final int ERROR_DOXYGEN_NOT_FOUND = 1;
/**
* Defines the pattern used to match doxygen warnings and errors
*/
private static final Pattern problemPattern = Pattern.compile("^(.+?):\\s*(\\d+)\\s*:\\s*(.+?)\\s*:\\s*(.*$(\\s+^ .*$)*)", Pattern.MULTILINE);
/**
* Defines the pattern used to match doxygen warnings about obsolete tags.
*/
private static final Pattern obsoleteTagWarningPattern = Pattern.compile("(?i)^warning: Tag `(.+)' at line (\\d+) of file `(.+)' has become obsolete.$", Pattern.MULTILINE);
/**
* Implements a runnable log feeder that reads the given input stream
* line per line and writes those lines back to the managed log. Once
* the stream end has been reached, the feeder exists.
*
* @author Guillaume Brocker
*/
private class MyLogFeeder implements Runnable
{
/**
* the input stream to read and write by to the log
*/
private InputStream input;
/**
* Constructor
*
* @param input the input stream to read to write back to the log
*/
public MyLogFeeder( InputStream input )
{
this.input = input;
}
public void run()
{
try
{
BufferedReader reader = new BufferedReader( new InputStreamReader(input) );
String newLine;
for(;;) {
// Processes a new process output line.
newLine = reader.readLine();
if( newLine != null ) {
newLine = newLine.concat( "\n" );
log.append( newLine );
fireLogUpdated( newLine );
}
else {
break;
}
}
}
catch( Throwable t )
{
Plugin.log( t );
}
}
}
/**
* Implements a resource change listener that will remove a given job if its
* doxyfile gets deleted.
*
* @author Guillaume Brocker
*/
private class MyResourceChangeListener implements IResourceChangeListener
{
/**
* the build job whose doxyfile will be monitored
*/
private BuildJob job;
public MyResourceChangeListener( BuildJob job )
{
this.job = job;
}
public void resourceChanged(IResourceChangeEvent event) {
IFile ifile = job.getDoxyfile().getIFile();
if (ifile != null) {
IResourceDelta doxyfileDelta = event.getDelta().findMember( ifile.getFullPath() );
if( doxyfileDelta != null && doxyfileDelta.getKind() == IResourceDelta.REMOVED )
{
job.clearMarkers();
jobs.remove( job );
job.fireRemoved();
ResourcesPlugin.getWorkspace().removeResourceChangeListener( this );
}
}
}
}
/**
* a string that is used to identify the doxygen build job family
*/
public static String FAMILY = "Doxygen Build Job";
/**
* a collection containing all created build jobs
*/
private static Collection<BuildJob> jobs = new HashSet<BuildJob>();
/**
* the path of the doxygen
*/
private String command;
/**
* the path of the doxyfile to build
*/
private Doxyfile doxyfile;
/**
* the buffer containing the whole build output log
*/
private StringBuffer log = new StringBuffer();
/**
* a set containing all registered build job listeners
*/
private Set<IBuildJobListener> listeners = new HashSet<IBuildJobListener>();
/**
* a collection containing all markers corresponding to doxygen warning and errors
*/
private Collection<IMarker> markers = new Vector<IMarker>();
/**
* Constructor.
*/
private BuildJob(Doxyfile dxfile) {
super("");
doxyfile = dxfile;
updateJobName();
setPriority( Job.BUILD );
setUser(true);
// References the jobs in the global collection and add a doxyfile listener.
jobs.add( this );
ResourcesPlugin.getWorkspace().addResourceChangeListener( new MyResourceChangeListener(this), IResourceChangeEvent.POST_CHANGE );
}
/**
* Retrieves all doxygen build jobs.
*
* @return an arry containing all doxygen build jobs (can be empty).
*/
public static BuildJob[] getAllJobs()
{
return (BuildJob[]) jobs.toArray( new BuildJob[0] );
}
/**
* Retrieves the build job associated to the given doxyfile. If needed,
* a new job will be created.
*
* @param doxyfile a given doxyfile instance
* @param doxygen a string containing the doxygen command to use
*
* @return a build job that is in charge of building the given doxyfile
*/
public static BuildJob getJob( Doxyfile doxyfile )
{
BuildJob result = findJob( doxyfile );
// If no jobs has been found, then creates a new one.
if( result == null )
{
result = new BuildJob(doxyfile);
}
// set new command
result.setCommand(Doxygen.getDefault().getCommand());
// Job's done.
return result;
}
/**
* Searches for a build job associated to the given doxyfile.
*
* @param doxyfile a given doxyfile instance
*
* @return a build job for the given doxyfile or null if none
*/
public static BuildJob findJob( Doxyfile doxyfile )
{
BuildJob result = null;
// Walks through the found jobs to find a relevant build job.
Iterator<BuildJob> i = jobs.iterator();
while( i.hasNext() )
{
BuildJob buildJob = (BuildJob) i.next();
if( buildJob.getDoxyfile().equals(doxyfile) )
{
result = buildJob;
break;
}
}
return result;
}
/**
* Adds the given listener to the job.
*
* @param listener a given listener instance
*/
public void addBuidJobListener( IBuildJobListener listener )
{
synchronized ( listeners ) {
listeners.add( listener );
}
}
/**
* Removes the given listener from the job.
*
* @param listener a given listener instance
*/
public void removeBuidJobListener( IBuildJobListener listener )
{
synchronized( listeners ) {
listeners.remove( listener );
}
}
/**
* Clears the log and notifies attached listeners
*/
public void clearLog() {
log.delete( 0, log.length() );
fireLogCleared();
}
/**
* Clears the markers managed by the build job.
*/
public void clearMarkers()
{
// Removes all markers from their respective resource
Iterator<IMarker> i = markers.iterator();
while( i.hasNext() )
{
IMarker marker = (IMarker) i.next();
try
{
marker.delete();
}
catch( Throwable t )
{
Plugin.log( t );
}
}
// Clear the marker collection
markers.clear();
}
/**
* Retrieves the doxyfile that is managed by the job.
*
* @return a file taht is the builded doxyfile
*/
public Doxyfile getDoxyfile() {
return doxyfile;
}
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
updateJobName();
}
public void updateJobName() {
setName("Doxygen Build ["+ command + " -b " + doxyfile.getFullPath()+"]");
}
/**
* Retrieves the job's whole log.
*
* @return a string containing the build job's log.
*/
public String getLog() {
return log.toString();
}
/**
* @see org.eclipse.core.runtime.jobs.Job#belongsTo(java.lang.Object)
*/
public boolean belongsTo(Object family) {
if( family == FAMILY )
{
return true;
}
else
{
return super.belongsTo( family );
}
}
/**
* @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
*/
protected IStatus run( IProgressMonitor monitor ) {
IFile doxyIFile = getDoxyfile().getIFile();
File doxyFile = getDoxyfile().getFile();
try {
// Initializes the progress monitor.
SubMonitor subMonitor = SubMonitor.convert(monitor, doxyfile.getFullPath(), 6);
// Clears log and markers.
// TODO: anb0s: this is not woking like expected no in Eclipse 4.x -> verify
clearLog();
clearMarkers();
subMonitor.worked( 1 );
// Locks access to the doxyfile.
if (doxyIFile != null) {
getJobManager().beginRule(doxyIFile, subMonitor);
}
// Creates the doxygen build process and log feeders.
Process buildProcess = null;
if (doxyIFile != null) {
buildProcess = Doxygen.getDefault().build(doxyIFile);
} else {
buildProcess = Doxygen.getDefault().build(doxyFile);
}
Thread inputLogFeeder = new Thread( new MyLogFeeder(buildProcess.getInputStream()) );
Thread errorLogFeeder = new Thread( new MyLogFeeder(buildProcess.getErrorStream()) );
// Wait either for the feeders to terminate or the user to cancel the job.
inputLogFeeder.start();
errorLogFeeder.start();
// TODO: anb0s: this is not woking like expected no in Eclipse 4.x -> verify
inputLogFeeder.join();
errorLogFeeder.join();
for(;;) {
// Tests of the log feeders have terminated.
if( inputLogFeeder.isAlive() == false && errorLogFeeder.isAlive() == false ) {
break;
}
// Tests if the jobs is supposed to terminate.
if( monitor.isCanceled() == true ) {
buildProcess.destroy();
buildProcess.waitFor();
if (doxyIFile != null) {
getJobManager().endRule(doxyIFile);
}
return Status.CANCEL_STATUS;
}
// Allows other threads to run.
// TODO: anb0s: does not work as expected on windows
Thread.yield();
Thread.sleep(1000L);
}
subMonitor.worked( 2 );
// Unlocks the doxyfile.
if (doxyIFile != null) {
getJobManager().endRule(doxyIFile);
doxyIFile = null;
}
// Builds error and warning markers
createMarkers( subMonitor );
subMonitor.worked( 3 );
// Ensure that doxygen process has finished.
buildProcess.waitFor();
subMonitor.worked( 4 );
// Refreshes the container that has received the documentation outputs.
Doxyfile parsedDoxyfile = getDoxyfile();
parsedDoxyfile.load();
IContainer outputContainer = parsedDoxyfile.getOutputContainer();
if( outputContainer != null ) {
outputContainer.refreshLocal( IResource.DEPTH_INFINITE, SubMonitor.convert(subMonitor, "Refresh doxygen output folder...", 1) );
}
subMonitor.done();
// Job's done.
return Status.OK_STATUS;
} catch(OperationCanceledException e) {
return Status.CANCEL_STATUS;
} catch( InvokeException e ) {
return new Status(
Status.WARNING,
Plugin.getDefault().getBundle().getSymbolicName(),
ERROR_DOXYGEN_NOT_FOUND,
"Doxygen was not found.",
e );
} catch( Throwable t ) {
return new Status(
Status.ERROR,
Plugin.getDefault().getBundle().getSymbolicName(),
0,
t.getMessage(),
t );
}
finally {
if (doxyIFile != null) {
getJobManager().endRule(doxyIFile);
}
}
}
/**
* Creates resource markers while finding warning and errors in the
* managed log.
*
* @param monitor the progress monitor used to watch for cancel requests.
* @throws CoreException, URISyntaxException
*/
private void createMarkers( IProgressMonitor monitor ) throws CoreException, URISyntaxException {
Matcher matcher = null;
// Searches documentation errors and warnings.
matcher = problemPattern.matcher( log );
while( matcher.find() == true ) {
Path resourcePath = new Path( matcher.group(1) );
Integer lineNumer = new Integer( matcher.group(2) );
int severity = Marker.toMarkerSeverity( matcher.group(3) );
String message = new String( matcher.group(4) );
createMarkersForResource(resourcePath, null, lineNumer, severity, message);
}
matcher = null;
// Searches obsolete tags warnings.
matcher = obsoleteTagWarningPattern.matcher( log );
while( matcher.find() == true ) {
String message = new String( matcher.group(0) );
String setting = new String( matcher.group(1) );
Integer lineNumer = new Integer( matcher.group(2) );
Path resourcePath = new Path( matcher.group(3) );
createMarkersForResource(resourcePath, setting, lineNumer, IMarker.SEVERITY_WARNING, message);
}
matcher = null;
}
private void createMarkersForResource(Path resourcePath, String setting, Integer lineNumer, int severity, String message) throws CoreException {
if (resourcePath != null) {
IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
IFile[] files = workspaceRoot.findFilesForLocationURI(URIUtil.toURI(resourcePath));
for (IFile file : files) {
IMarker marker = Marker.create(file, setting, lineNumer.intValue(), message, severity);
if( marker != null ) {
markers.add(marker);
break; // exit loop
}
}
}
}
/**
* Notifies observers that the log has been cleared.
*/
private void fireLogCleared() {
synchronized ( listeners ) {
Iterator<IBuildJobListener> i = listeners.iterator();
while( i.hasNext() ) {
IBuildJobListener listener = (IBuildJobListener) i.next();
listener.buildJobLogCleared( this );
}
}
}
/**
* Notifies observers that the log has been updated with new text.
*
* @param newText a string containing the new text of the log
*/
private void fireLogUpdated( String newText ) {
synchronized ( listeners ) {
Iterator<IBuildJobListener> i = listeners.iterator();
while( i.hasNext() ) {
IBuildJobListener listener = (IBuildJobListener) i.next();
listener.buildJobLogUpdated( this, newText );
}
}
}
/**
* Notifies observers that the job has been removed.
*/
private void fireRemoved() {
synchronized ( listeners ) {
Iterator<IBuildJobListener> i = listeners.iterator();
while( i.hasNext() ) {
IBuildJobListener listener = (IBuildJobListener) i.next();
listener.buildJobRemoved( this );
}
}
}
}