package com.baselet.plugin.builder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.swing.ProgressMonitor;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
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.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.JavaCore;
import com.baselet.diagram.DiagramHandler;
import com.baselet.diagram.io.OutputHandler;
import com.baselet.plugin.UmletPluginUtils;
import com.baselet.plugin.refactoring.ImageReference;
import com.baselet.plugin.refactoring.JavaDocParser.SourceString;
/**
* Builder creating a png for each uxf found in the project. In addition,
* warnings are created for image references pointing to inexistant images.
*
* <ul>
* <li> The png is only updated when the uxf is newer than the png </li>
* <li> a clean removes all pngs with corresponding uxfs</li>
* <li> a full build checks all uxfs</li>
* <li> incremental build is supported</li>
* <li> diagrams are converted in parallel </li>
* </ul>
*/
public class UmletBuilder extends IncrementalProjectBuilder {
public static final String BUILDER_ID = "com.umlet.plugin.umletBuilder";
public static final String PROBLEM_MARKER_TYPE = "com.umlet.plugin.builderProblem";
public static final String IMG_MISSING_MARKER_TYPE = "com.umlet.plugin.imgMissing";
private static class ResourceSet {
List<IFile> uxfFiles = new ArrayList<IFile>();
List<ICompilationUnit> units = new ArrayList<ICompilationUnit>();
public void handle(IResource res) {
IFile file = res.getAdapter(IFile.class);
if (file == null) {
return;
}
if (file.getName().endsWith(".uxf")) {
if (!file.isDerived(IResource.CHECK_ANCESTORS) && file.exists()) {
uxfFiles.add(file);
}
return;
}
IJavaElement element = JavaCore.create(file);
if (element instanceof ICompilationUnit) {
units.add((ICompilationUnit) element);
}
}
public int size() {
return uxfFiles.size() + units.size();
}
}
@Override
protected IProject[] build(final int kind, final Map<String, String> args, final IProgressMonitor monitor)
throws CoreException {
// collect resources
ResourceSet resources = new ResourceSet();
try {
if (kind == FULL_BUILD) {
collectFullBuildResources(resources);
}
else {
IResourceDelta delta = getDelta(getProject());
if (delta == null) {
collectFullBuildResources(resources);
}
else {
collectIncrementalBuildResources(delta, resources);
}
}
} catch (CoreException e) {
throw new RuntimeException(e);
}
// process resources
processResources(resources, monitor);
return null;
}
@Override
protected void clean(IProgressMonitor monitor) throws CoreException {
IProject project = getProject();
project.accept(new IResourceVisitor() {
@Override
public boolean visit(IResource res) throws CoreException {
if (res instanceof IFile) {
IFile png = (IFile) res;
if ("png".equalsIgnoreCase(png.getFileExtension())) {
IFile uxf = UmletPluginUtils.getUxfDiagramForImgFile(png);
if (uxf.exists()) {
// clean only pngs with corresponding umlet diagrams
png.delete(true, null);
}
}
}
return true;
}
});
}
private void collectIncrementalBuildResources(IResourceDelta delta, final ResourceSet resources) throws CoreException {
delta.accept(new IResourceDeltaVisitor() {
@Override
public boolean visit(IResourceDelta delta) {
resources.handle(delta.getResource());
// visit children too
return true;
}
});
}
private void collectFullBuildResources(final ResourceSet resources) throws CoreException {
getProject().accept(new IResourceVisitor() {
@Override
public boolean visit(IResource res) throws CoreException {
resources.handle(res);
// visit children too
return true;
}
});
}
private void processResources(ResourceSet resources, final IProgressMonitor monitor) {
if (monitor != null) {
monitor.beginTask("Update Umlet Diagrams", resources.size());
}
// create thread pool to process the diagrams in parallel
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
try {
List<ExportTask> tasks = new ArrayList<ExportTask>();
final Object monitorLock = new Object();
final LinkedHashSet<String> diagramsInProgress = new LinkedHashSet<String>();
// create a task for each resource. This will submit them to the executor
for (final IResource resource : resources.uxfFiles) {
tasks.add(new ExportTask(resource, executor, monitor, monitorLock, diagramsInProgress));
}
// await finishing all tasks. This also causes the resource refresh and placing problem markers
for (ExportTask task : tasks) {
task.awaitFinish();
}
} finally {
// shutdown the pool
executor.shutdownNow();
}
// process compilation units and create makers for missing images
for (ICompilationUnit cu : resources.units) {
if (monitor != null) {
monitor.subTask("processing " + cu.getElementName());
}
try {
if (!cu.exists()) {
continue;
}
IResource correspondingResource = cu.getCorrespondingResource();
if (correspondingResource == null) {
continue;
}
correspondingResource.deleteMarkers(IMG_MISSING_MARKER_TYPE, false, IResource.DEPTH_INFINITE);
for (ImageReference reference : UmletPluginUtils.collectAllImageRefs(cu)) {
SourceString srcAttrValue = reference.srcAttr.value;
IPath path = UmletPluginUtils.getRootRelativePath(cu, srcAttrValue.getValue());
IFile imageFile = UmletPluginUtils.getFile(UmletPluginUtils.getPackageFragmentRoot(cu), path);
if (!imageFile.exists()) {
IMarker marker = correspondingResource.createMarker(IMG_MISSING_MARKER_TYPE);
marker.setAttribute(IMarker.MESSAGE, "Unable to find referenced image " + path);
marker.setAttribute(IMarker.LOCATION, "JavaDoc");
marker.setAttribute(IMarker.CHAR_START, srcAttrValue.start);
marker.setAttribute(IMarker.CHAR_END, srcAttrValue.end + 1);
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_WARNING);
}
}
} catch (CoreException e) {
throw new RuntimeException(e);
} finally {
if (monitor != null) {
monitor.worked(1);
}
}
}
if (monitor != null) {
monitor.done();
}
}
/**
* Encapsulates the state necessary to export a single file. Handles the {@link ProgressMonitor}
*/
private static class ExportTask {
private IFile outFile;
private IFile inFile;
private final File inputFile;
private final Future<byte[]> future;
public ExportTask(IResource res, ExecutorService exec, final IProgressMonitor monitor, final Object monitorLock, final LinkedHashSet<String> diagramsInProgress) {
inFile = res.getAdapter(IFile.class);
String baseName = inFile.getName().substring(0, inFile.getName().length() - 4);
IContainer parent = inFile.getParent();
{
IProject p = parent.getAdapter(IProject.class);
if (p != null) {
outFile = p.getFile(baseName + ".png");
}
}
if (outFile == null) {
IFolder f = parent.getAdapter(IFolder.class);
if (f != null) {
outFile = f.getFile(baseName + ".png");
}
}
if (outFile == null) {
throw new RuntimeException("unable to determine target location for " + inFile);
}
inputFile = new File(inFile.getLocationURI());
future = exec.submit(new Callable<byte[]>() {
@Override
public byte[] call() throws Exception {
if (monitor != null) {
synchronized (monitorLock) {
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
diagramsInProgress.add(inFile.getName());
updateSubTask();
}
}
try {
// only export if inFile is newer than outFile
// if (!outFile.exists() || inFile.getModificationStamp() > outFile.getModificationStamp()) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
DiagramHandler handler = new DiagramHandler(inputFile);
OutputHandler.createToStream("png", os, handler);
os.close();
return os.toByteArray();
// }
// return null;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (monitor != null) {
synchronized (monitorLock) {
monitor.worked(1);
diagramsInProgress.remove(inFile.getName());
updateSubTask();
}
}
}
}
private void updateSubTask() {
if (monitor == null) {
return;
}
StringBuilder sb = new StringBuilder();
for (String s : diagramsInProgress) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(s);
}
monitor.subTask("Processing Diagrams " + sb.toString());
}
});
}
/**
* Wait until the export has finished and create/remove markers
*/
void awaitFinish() throws OperationCanceledException {
try {
// remove markers
try {
if (inFile.exists()) {
inFile.deleteMarkers(PROBLEM_MARKER_TYPE, false, IResource.DEPTH_INFINITE);
}
} catch (CoreException e1) {
throw new RuntimeException(e1);
}
// wait for task to complete
byte[] bb = future.get();
// write result
if (bb != null) {
try {
if (outFile.exists()) {
outFile.setContents(new ByteArrayInputStream(bb), false, true, null);
}
else {
outFile.create(new ByteArrayInputStream(bb), true, null);
}
outFile.setDerived(true, null);
} catch (CoreException e) {
throw new ExecutionException(e);
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
if (e.getCause() instanceof OperationCanceledException) {
throw (OperationCanceledException) e.getCause();
}
try {
if (inFile.exists()) {
IMarker marker = inFile.createMarker(PROBLEM_MARKER_TYPE);
marker.setAttribute(IMarker.MESSAGE, "Error while exporting Umlet diagram: " + e.getCause().getMessage());
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
}
} catch (CoreException e1) {
throw new RuntimeException(e1);
}
}
}
}
}