/******************************************************************************* * Copyright (c) 2013 Bruno Medeiros and other Contributors. * 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: * Bruno Medeiros - initial API and implementation *******************************************************************************/ package mmrnmhrm.core.dub_model; import static melnorme.lang.ide.core.operations.ILangOperationsListener_Default.ProcessStartKind.ENGINE_TOOLS; import static melnorme.lang.ide.core.utils.TextMessageUtils.headerBIG; import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull; import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue; import java.text.MessageFormat; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRunnable; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.jobs.Job; import dtool.dub.DubBundle; import dtool.dub.DubBundle.DubBundleException; import dtool.dub.DubBundleDescription; import dtool.dub.DubDescribeRunner; import dtool.dub.DubManifestParser; import dtool.engine.compiler_installs.CompilerInstall; import dtool.engine.compiler_installs.SearchCompilersOnPathOperation; import melnorme.lang.ide.core.LangCore; import melnorme.lang.ide.core.operations.ILangOperationsListener_Default.IToolOperationMonitor; import melnorme.lang.ide.core.operations.ToolManager; import melnorme.lang.ide.core.project_model.BundleModelManager; import melnorme.lang.ide.core.project_model.LangBundleModel; import melnorme.lang.ide.core.utils.EclipseUtils; import melnorme.lang.ide.core.utils.ResourceUtils; import melnorme.lang.ide.core.utils.operation.EclipseAsynchJobAdapter; import melnorme.lang.ide.core.utils.operation.EclipseAsynchJobAdapter.IRunnableWithJob; import melnorme.lang.tooling.BundlePath; import melnorme.lang.tooling.bundle.BundleInfo; import melnorme.utilbox.concurrency.CancellableTask; import melnorme.utilbox.concurrency.ITaskAgent; import melnorme.utilbox.concurrency.OperationCancellation; import melnorme.utilbox.core.CommonException; import mmrnmhrm.core.DeeCoreMessages; import mmrnmhrm.core.dub_model.DeeBundleModelManager.DeeBundleModel; import mmrnmhrm.core.dub_model.DeeBundleModelManager.WorkspaceModelManagerTask; import mmrnmhrm.core.engine.DeeToolManager; /** * Updates a {@link DeeBundleModel} when resource changes occur, using 'dub describe'. * Also creates problem markers on the Eclipse workspace. */ public class DeeBundleModelManager extends BundleModelManager<DeeBundleModel> { public static class DeeBundleModel extends LangBundleModel { @Override public BundleInfo setBundleInfo(IProject project, BundleInfo newProjectInfo) { return super.setBundleInfo(project, newProjectInfo); } } /* ----------------- ----------------- */ public static final String DUB_PROBLEM_ID = LangCore.PLUGIN_ID + ".DubProblem"; public DeeBundleModelManager() { super(new DeeBundleModel()); } public DeeToolManager getProcessManager() { return (DeeToolManager) LangCore.getToolManager(); } @Override protected ManagerResourceListener init_createResourceListener() { return new ManagerResourceListener(); } @Override protected void handleBundleProjectAdded(IProject project) { handleBundleManifestChanged(project); } protected void handleBundleManifestChanged(final IProject project) { BundleInfo unresolvedProjectInfo = createNewInfo(project); getModel().setBundleInfo(project, unresolvedProjectInfo); modelAgent.submitTask(new ProjectModelDubDescribeTask(this, project, unresolvedProjectInfo)); } @Override protected BundleInfo createNewInfo(IProject project) { DubBundleDescription unresolvedDescription = readUnresolvedBundleDescription(project); if(unresolvedDescription.hasErrors() && unresolvedDescription.isParseError()) { // Remove the parse error - we will run `dub describe` anyways, as DUB might still be able to parse it. unresolvedDescription = new DubBundleDescription(unresolvedDescription.getMainBundle(), (DubBundleException) null); } /* XXX: Could it be a problem to run a possibly long-running operation here? */ return createProjectInfo(unresolvedDescription); } protected DubBundleDescription readUnresolvedBundleDescription(final IProject project) { java.nio.file.Path location = project.getLocation().toFile().toPath(); BundlePath bundlePath = BundlePath.create(location); DubBundle unresolvedBundle = DubManifestParser.parseDubBundleFromLocation2(bundlePath); if(unresolvedBundle == null) { // Can happen if using SDL format, which we don't know how to parse. Provide dummy bundle String message = "Cannot read bundle (is SDL format instead of JSON?"; unresolvedBundle = new DubBundle( bundlePath, "(UNKNOWN)", new DubBundleException(message), null, null, null, null, null, null, null, null ); } return new DubBundleDescription(unresolvedBundle); } protected final void updateProjectInfo(IProject project, BundleInfo oldInfo, DubBundleDescription dubBundleDescription) { getModel().updateProjectInfo(project, oldInfo, createProjectInfo(dubBundleDescription)); } /* ----------------------------------- */ protected class SearchCompilersOnPathOperation_Eclipse extends SearchCompilersOnPathOperation { @Override protected void handleWarning(String message) { LangCore.logWarning(message); } } protected BundleInfo createProjectInfo(DubBundleDescription dubBundleDescription) { CompilerInstall compilerInstall = new SearchCompilersOnPathOperation_Eclipse(). searchForCompilersInDefaultPathEnvVars().getPreferredInstall(); return new BundleInfo(compilerInstall, dubBundleDescription); } /** * Await for all pending resolutions to complete. * Of use mainly for test code. */ public void syncPendingUpdates() throws InterruptedException { modelAgent.waitForPendingTasks(); } /** WARNING: this API is intended to be used for tests only */ public ITaskAgent internal_getModelAgent() { return modelAgent; } public static IMarker[] getDubErrorMarkers(IProject project) throws CoreException { return project.findMarkers(DUB_PROBLEM_ID, true, IResource.DEPTH_ONE); } protected abstract class WorkspaceModelManagerTask extends CancellableTask{ protected final DeeBundleModelManager workspaceModelManager; public WorkspaceModelManagerTask() { this.workspaceModelManager = DeeBundleModelManager.this; } protected void logInternalError(Throwable t) { LangCore.logInternalError(t); } } } class ProjectModelDubDescribeTask extends ProjectUpdateBuildpathTask implements IRunnableWithJob { protected final IProject project; protected final BundleInfo unresolvedProjectInfo; protected final DubBundleDescription unresolvedDescription; protected ProjectModelDubDescribeTask(DeeBundleModelManager dubModelManager, IProject project, BundleInfo unresolvedProjectInfo) { super(dubModelManager); this.project = project; this.unresolvedProjectInfo = unresolvedProjectInfo; unresolvedDescription = unresolvedProjectInfo.getBundleDesc(); } protected DeeToolManager getProcessManager() { return workspaceModelManager.getProcessManager(); } @Override protected void doRun() { try { ResourceUtils.getWorkspace().run(new IWorkspaceRunnable() { @Override public void run(IProgressMonitor monitor) throws CoreException { if(project.exists() == false) { return; } deleteDubMarkers(project); if(unresolvedDescription.hasErrors()) { setDubErrorMarker(project, unresolvedDescription.getError()); } } }, project, 0, null); } catch (CoreException ce) { logInternalError(ce); } if(unresolvedDescription.hasErrors()) { // don't run `dub describe` if there was critical errors, just let the markers be updated return; } try { // Job job = EclipseAsynchJobAdapter.runUnderAsynchJob(getNameForJob(), this); // Note: we await the job to finish, so that the describe task is synchronous to the bundle resolution // this is so that DeeBundleModelManager.syncPendingUpdates() works. // Ideally this would be written in a more elegant way, with Future's etc. job.join(); } catch (InterruptedException e) { return; } } protected String getNameForJob() { return "Running 'dub describe' on project: " + project.getName(); } protected void deleteDubMarkers(IProject project) throws CoreException { IMarker[] markers = DeeBundleModelManager.getDubErrorMarkers(project); for (IMarker marker : markers) { marker.delete(); } } protected void setDubErrorMarker(IProject project, DubBundleException error) throws CoreException { setDubErrorMarker(project, error.getExtendedMessage()); } protected void setDubErrorMarker(IProject project, String message) throws CoreException { IMarker dubMarker = project.createMarker(DeeBundleModelManager.DUB_PROBLEM_ID); dubMarker.setAttribute(IMarker.MESSAGE, message); dubMarker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR); } @Override public void runUnderEclipseJob(IProgressMonitor monitor) { assertNotNull(monitor); try { try { resolveProjectOperation(monitor); } catch(CoreException e) { throw EclipseUtils.createCommonException(e); } } catch(OperationCancellation ce) { return; } catch(CommonException ce) { try { EclipseUtils.getWorkspace().run(new IWorkspaceRunnable() { @Override public void run(IProgressMonitor monitor) throws CoreException { if(project.exists() == false) { return; } setProjectDubError(project, ce); } }, null, 0, monitor); } catch (CoreException e) { logInternalError(ce); } } } protected ToolManager getToolManager() { return LangCore.getToolManager(); } protected Void resolveProjectOperation(IProgressMonitor pm) throws CoreException, CommonException, OperationCancellation { IPath projectLocation = project.getLocation(); if(projectLocation == null) { return null; // Project no longer exists, or not stored in the local filesystem. } BundlePath bundlePath = BundlePath.create(projectLocation.toFile().toPath()); String dubPath = getToolManager().getSDKToolPath(project).toString(); IToolOperationMonitor opMonitor = getToolManager().startNewOperation(ENGINE_TOOLS, true, false); opMonitor.writeInfoMessage( headerBIG(MessageFormat.format(DeeCoreMessages.RunningDubDescribe, project.getName()))); // TODO: add --skip-registry to dub command final DubBundleDescription describedBundle = new DubDescribeRunner(bundlePath, dubPath, true) .runDescribeOperation(); if(describedBundle.hasErrors()) { throw new CommonException("DUB describe error: ", describedBundle.getError()); } DubBundleDescription bundleDesc = getEffectiveBundleDescription(describedBundle); ResourceUtils.getWorkspace().run(new IWorkspaceRunnable() { @Override public void run(IProgressMonitor monitor) throws CoreException { if(project.exists() == false) { return; } assertTrue(!bundleDesc.hasErrors()); deleteDubMarkers(project); workspaceModelManager.updateProjectInfo(project, unresolvedProjectInfo, bundleDesc); project.refreshLocal(1, monitor); } }, null, 0, pm); return null; } protected DubBundleDescription getEffectiveBundleDescription(final DubBundleDescription describedBundle) { // Because `dub describe` does not supply configuration info, we take the described bundle // and add to it the configuration info from the previous, parsed description. DubBundle mainBundle = describedBundle.getMainBundle(); mainBundle = new DubBundle( mainBundle.getBundlePath(), mainBundle.getBundleName(), mainBundle.error, mainBundle.version, mainBundle.srcFolders, mainBundle.getEffectiveSourceFolders(), mainBundle.bundleFiles, mainBundle.getDependencyRefs(), mainBundle.getTargetName(), mainBundle.getTargetPath(), unresolvedDescription.getMainBundle().getConfigurations().toArrayList() // add configs here ); return new DubBundleDescription(mainBundle, describedBundle.getBundleDependencies()); } protected void setProjectDubError(IProject project, CommonException ce) throws CoreException { DubBundleException dubError = new DubBundleException(ce.getMessage(), ce.getCause()); DubBundle main = unresolvedDescription.getMainBundle(); DubBundleDescription bundleDesc = new DubBundleDescription(main, dubError); BundleInfo newProjectInfo = new BundleInfo(unresolvedProjectInfo.getCompilerInstall(), bundleDesc); workspaceModelManager.getModel().setBundleInfo(project, newProjectInfo); setDubErrorMarker(project, dubError); } } abstract class ProjectUpdateBuildpathTask extends WorkspaceModelManagerTask { protected ProjectUpdateBuildpathTask(DeeBundleModelManager dubModelManager) { dubModelManager.super(); } }