Java tutorial
/******************************************************************************* * Copyright (c) 2015, 2019 Pivotal, Inc. * 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 * https://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.dash.cloudfoundry; import static org.springframework.ide.eclipse.boot.dash.model.AbstractLaunchConfigurationsDashElement.READY_STATES; import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.net.URISyntaxException; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; import org.cloudfoundry.util.JobUtils; import org.eclipse.compare.CompareConfiguration; import org.eclipse.compare.CompareEditorInput; import org.eclipse.compare.structuremergeviewer.DiffNode; import org.eclipse.core.filebuffers.FileBuffers; import org.eclipse.core.filebuffers.ITextFileBuffer; import org.eclipse.core.filebuffers.LocationKind; import org.eclipse.core.resources.IFile; 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.IResourceDelta; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; 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.jobs.Job; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.osgi.framework.Version; import org.springframework.ide.eclipse.boot.dash.BootDashActivator; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFApplication; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFApplicationDetail; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFCloudDomain; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.ClientRequests; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2.ReactorUtils; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.console.CloudAppLogManager; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.debug.DebugSupport; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.CloudApplicationDeploymentProperties; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeploymentPropertiesDialog; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.YamlFileInput; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.YamlGraphDeploymentProperties; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.YamlInput; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.ops.ConnectOperation; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.ops.JobBody; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.ops.Operation; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.ops.OperationsExecution; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.ops.ProjectsDeployer; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.ops.TargetApplicationsRefreshOperation; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.packaging.CloudApplicationArchiverStrategies; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.packaging.CloudApplicationArchiverStrategy; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.packaging.ICloudApplicationArchiver; import org.springframework.ide.eclipse.boot.dash.dialogs.DeploymentPropertiesDialogModel; import org.springframework.ide.eclipse.boot.dash.dialogs.DeploymentPropertiesDialogModel.ManifestType; import org.springframework.ide.eclipse.boot.dash.dialogs.ManifestDiffDialogModel; import org.springframework.ide.eclipse.boot.dash.dialogs.ManifestDiffDialogModel.Result; import org.springframework.ide.eclipse.boot.dash.dialogs.UnsupportedPushProperties; import org.springframework.ide.eclipse.boot.dash.livexp.DisposingFactory; import org.springframework.ide.eclipse.boot.dash.livexp.LiveSets; import org.springframework.ide.eclipse.boot.dash.model.AbstractBootDashModel; import org.springframework.ide.eclipse.boot.dash.model.AsyncDeletable; import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; import org.springframework.ide.eclipse.boot.dash.model.BootDashModelContext; import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; import org.springframework.ide.eclipse.boot.dash.model.Deletable; import org.springframework.ide.eclipse.boot.dash.model.ModifiableModel; import org.springframework.ide.eclipse.boot.dash.model.RefreshState; import org.springframework.ide.eclipse.boot.dash.model.RunState; import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.CannotAccessPropertyException; import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetType; import org.springframework.ide.eclipse.boot.dash.views.BootDashModelConsoleManager; import org.springframework.ide.eclipse.boot.pstore.IPropertyStore; import org.springframework.ide.eclipse.boot.pstore.PropertyStores; import org.springframework.ide.eclipse.boot.util.Log; import org.springsource.ide.eclipse.commons.core.util.StringUtil; import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil; import org.springsource.ide.eclipse.commons.frameworks.core.util.JobUtil; import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression; import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; import org.springsource.ide.eclipse.commons.livexp.core.DisposeListener; import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; import org.yaml.snakeyaml.Yaml; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class CloudFoundryBootDashModel extends AbstractBootDashModel implements ModifiableModel { private IPropertyStore modelStore; public static final String APP_TO_PROJECT_MAPPING = "projectToAppMapping"; private static final Comparator<BootDashElement> ELEMENT_COMPARATOR = new Comparator<BootDashElement>() { @Override public int compare(BootDashElement o1, BootDashElement o2) { int cat1 = getCategory(o1); int cat2 = getCategory(o2); if (cat1 != cat2) { return cat1 - cat2; } else { return o1.getName().compareTo(o2.getName()); } } private int getCategory(BootDashElement o1) { if (o1 instanceof CloudAppDashElement) { return 1; } else if (o1 instanceof CloudServiceInstanceDashElement) { return 2; } else { //Not really possible but anyhow... return 999; } } }; private CloudDashElementFactory elementFactory; private UnsupportedPushProperties unsupportedPushProperties; private final LiveSetVariable<CloudServiceInstanceDashElement> services = new LiveSetVariable<>(AsyncMode.SYNC); private final CloudDashApplications applications = new CloudDashApplications(this); private final ObservableSet<BootDashElement> allElements = LiveSets.union(applications.getApplications(), services); private BootDashModelConsoleManager consoleManager; private DevtoolsDebugTargetDisconnector debugTargetDisconnector; private LiveVariable<RefreshState> baseRefeshState = new LiveVariable<>(); private LiveExpression<RefreshState> apiWarning = new AsyncLiveExpression<RefreshState>(RefreshState.READY, "Check CC api version") { { dependsOn(getClientExp()); } @Override protected RefreshState compute() { try { ClientRequests client = getClient(); if (client != null) { Version server = client.getApiVersion(); Version supported = client.getSupportedApiVersion(); if (server.compareTo(supported) < 0) { return RefreshState.warning("Client supports API version " + server + " and is connected to server with API version " + supported + ". " + "Things may not work as expected."); } } return RefreshState.READY; } catch (Exception e) { return RefreshState.error(e); } } }; private LiveExpression<RefreshState> refreshState = new LiveExpression<RefreshState>() { { dependsOn(baseRefeshState); dependsOn(apiWarning); addListener((e, v) -> notifyModelStateChanged()); } @Override protected RefreshState compute() { return RefreshState.merge(baseRefeshState.getValue(), apiWarning.getValue()); } }; final private IResourceChangeListener resourceChangeListener = new IResourceChangeListener() { @Override public void resourceChanged(IResourceChangeEvent event) { try { if (event.getDelta() == null && event.getSource() != ResourcesPlugin.getWorkspace()) { return; } /* * Collect data on renamed and removed projects */ Map<IPath, IProject> renamedFrom = new HashMap<>(); Map<IPath, IProject> renamedTo = new HashMap<>(); List<IProject> removedProjects = new ArrayList<>(); for (IResourceDelta delta : event.getDelta().getAffectedChildren( IResourceDelta.CHANGED | IResourceDelta.ADDED | IResourceDelta.REMOVED)) { IResource resource = delta.getResource(); if (resource instanceof IProject) { IProject project = (IProject) resource; if (delta.getKind() == IResourceDelta.REMOVED) { if ((delta.getFlags() & IResourceDelta.MOVED_TO) != 0) { renamedFrom.put(delta.getMovedToPath(), project); } else { removedProjects.add(project); } } else if (delta.getKind() == IResourceDelta.ADDED && (delta.getFlags() & IResourceDelta.MOVED_FROM) != 0) { renamedTo.put(project.getFullPath(), project); } } } /* * Update CF app cache and collect apps that have local project * updated */ List<CloudAppDashElement> appsToRefresh = new ArrayList<>(); for (IProject project : removedProjects) { CloudAppDashElement app = getApplication(project); if (app != null && app.setProject(null)) { appsToRefresh.add(app); } } for (Map.Entry<IPath, IProject> entry : renamedFrom.entrySet()) { IPath path = entry.getKey(); IProject oldProject = entry.getValue(); IProject newProject = renamedTo.get(path); if (oldProject != null) { CloudAppDashElement app = getApplication(oldProject); if (app != null && app.setProject(newProject)) { appsToRefresh.add(app); } } } /* * Update BDEs */ for (CloudAppDashElement app : appsToRefresh) { notifyElementChanged(app, "resourceChanged"); } } catch (OperationCanceledException oce) { Log.log(oce); } catch (Exception e) { Log.log(e); } } }; public RefreshState getRefreshState() { return refreshState.getValue(); } final private ValueListener<ClientRequests> RUN_TARGET_CONNECTION_LISTENER = new ValueListener<ClientRequests>() { @Override public void gotValue(LiveExpression<ClientRequests> exp, ClientRequests value) { CloudFoundryBootDashModel.this.notifyModelStateChanged(); } }; private DisposingFactory<BootDashElement, LiveExpression<URI>> actuatorUrlFactory; public CloudFoundryBootDashModel(CloudFoundryRunTarget target, BootDashModelContext context, BootDashViewModel parent) { super(target, parent); RunTargetType type = target.getType(); IPropertyStore typeStore = PropertyStores.createForScope(type, context.getRunTargetProperties()); this.modelStore = PropertyStores.createSubStore(target.getId(), typeStore); this.elementFactory = new CloudDashElementFactory(context, modelStore, this); this.consoleManager = new CloudAppLogManager(target); this.unsupportedPushProperties = new UnsupportedPushProperties(); this.debugTargetDisconnector = DevtoolsUtil.createDebugTargetDisconnector(this); getRunTarget().addConnectionStateListener(RUN_TARGET_CONNECTION_LISTENER); ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceChangeListener, IResourceChangeEvent.POST_CHANGE); try { if (getRunTarget().getTargetProperties().get(CloudFoundryTargetProperties.DISCONNECTED) == null && (getRunTarget().getTargetProperties().isStoreCredentials() || getRunTarget().getTargetProperties().getCredentials() != null)) { // If CF target was connected previously and either password is stored or not stored but non-null then connect automatically getOperationsExecution().runAsynch(new ConnectOperation(this, true)); } } catch (CannotAccessPropertyException e) { // ignore shouldn't happen. Get password is called only if password not stored } } @Override public ObservableSet<BootDashElement> getElements() { return allElements; } public DisposingFactory<BootDashElement, LiveExpression<URI>> getActuatorUrlFactory() { if (actuatorUrlFactory == null) { this.actuatorUrlFactory = new DisposingFactory<BootDashElement, LiveExpression<URI>>(getElements()) { protected LiveExpression<URI> create(final BootDashElement key) { final LiveExpression<URI> uriExp = new LiveExpression<URI>() { protected URI compute() { try { RunState runstate = key.getRunState(); if (READY_STATES.contains(runstate)) { String host = key.getLiveHost(); if (StringUtil.hasText(host)) { return new URI("https://" + host); } } } catch (URISyntaxException e) { Log.log(e); } return null; } }; final ElementStateListener elementListener = new ElementStateListener() { public void stateChanged(BootDashElement e) { if (e.equals(key)) { uriExp.refresh(); } } }; uriExp.onDispose(new DisposeListener() { public void disposed(Disposable disposed) { removeElementStateListener(elementListener); } }); addElementStateListener(elementListener); return uriExp; } }; addDisposableChild(actuatorUrlFactory); } return actuatorUrlFactory; } @Override public void dispose() { getRunTarget().removeConnectionStateListener(RUN_TARGET_CONNECTION_LISTENER); if (debugTargetDisconnector != null) { debugTargetDisconnector.dispose(); debugTargetDisconnector = null; } applications.dispose(); ResourcesPlugin.getWorkspace().removeResourceChangeListener(resourceChangeListener); super.dispose(); } @Override public void refresh(UserInteractions ui) { runAsynch(new TargetApplicationsRefreshOperation(this, ui), ui); runAsynch(new ServicesRefreshOperation(this), ui); } @Override public Comparator<BootDashElement> getElementComparator() { return ELEMENT_COMPARATOR; } @Override public CloudFoundryRunTarget getRunTarget() { return (CloudFoundryRunTarget) super.getRunTarget(); } @Override public boolean canBeAdded(List<Object> sources) { if (sources != null && !sources.isEmpty() && getRunTarget().isConnected()) { for (Object obj : sources) { // IMPORTANT: to avoid drag/drop into the SAME target, be // sure // all sources are from a different target if (getProject(obj) == null || !isFromDifferentTarget(obj)) { return false; } } return true; } return false; } @Override public void add(List<Object> sources, UserInteractions ui) throws Exception { Builder<IProject> projects = ImmutableSet.builder(); if (sources != null) { for (Object obj : sources) { IProject project = getProject(obj); if (project != null) { projects.add(project); } } performDeployment(projects.build(), ui, RunState.RUNNING); } } protected IProject getProject(Object obj) { IProject project = null; if (obj instanceof IProject) { project = (IProject) obj; } else if (obj instanceof IJavaProject) { project = ((IJavaProject) obj).getProject(); } else if (obj instanceof IAdaptable) { project = (IProject) ((IAdaptable) obj).getAdapter(IProject.class); } else if (obj instanceof BootDashElement) { project = ((BootDashElement) obj).getProject(); } return project; } protected boolean isFromDifferentTarget(Object dropSource) { if (dropSource instanceof BootDashElement) { return ((BootDashElement) dropSource).getBootDashModel() != this; } // If not a boot element that is being dropped, it is an element // external to the boot dash view (e.g. project from project explorer) return true; } public void performDeployment(final Set<IProject> projectsToDeploy, final UserInteractions ui, RunState runOrDebug) throws Exception { DebugSupport debugSuppport = getViewModel().getCfDebugSupport(); runAsynch(new ProjectsDeployer(CloudFoundryBootDashModel.this, ui, projectsToDeploy, runOrDebug, debugSuppport), ui); } public CloudAppDashElement addElement(CFApplicationDetail appDetail, IProject project) throws Exception { CloudAppDashElement addedElement = null; boolean changed = false; synchronized (this) { addedElement = applications.addApplication(appDetail.getName()); addedElement.setDetailedData(appDetail); // Update the cache BEFORE updating the model, since the model // elements are handles to the cache changed = addedElement.setProject(project); //Should be okay to call inside synch block as the events are fired from a // a Job now. } if (changed) { notifyElementChanged(addedElement, "addElement detected setProject caused a change"); } return addedElement; } public CloudAppDashElement getApplication(String appName) { Set<CloudAppDashElement> apps = getApplications().getValues(); for (CloudAppDashElement element : apps) { if (appName.equals(element.getName())) { return element; } } return null; } public CloudServiceInstanceDashElement getService(String serviceName) { ImmutableSet<CloudServiceInstanceDashElement> services = getServices().getValues(); for (CloudServiceInstanceDashElement s : services) { if (s.getName().equals(serviceName)) { return s; } } return null; } private CloudAppDashElement getApplication(IProject project) { Set<CloudAppDashElement> apps = getApplications().getValues(); boolean includeNonExistingProjects = !project.exists(); for (CloudAppDashElement element : apps) { if (project.equals(element.getProject(includeNonExistingProjects))) { return element; } } return null; } public CloudAppDashElement ensureApplication(String appName) { synchronized (this) { return applications.addApplication(appName); } } public void removeApplication(String appName) { synchronized (this) { applications.removeApplication(appName); } } /** * Perform a 'shallow' update of the application elements in this model. This only * ensures that elements with the right names exist, creating needed ones and * disposing removed ones, but reusing existing ones. The state of the existing elements * is not updated in any way. */ public void updateAppNames(Collection<String> names) { applications.setAppNames(names); } public void updateElements(Collection<CFApplicationDetail> apps) throws Exception { if (apps == null) { /* * Error case: set empty list of BDEs don't modify state of local to CF artifacts mappings */ applications.setAppNames(ImmutableSet.<String>of()); } else { synchronized (this) { applications.setAppNames(getNames(apps)); for (CFApplicationDetail appDetails : apps) { CloudAppDashElement app = applications.getApplication(appDetails.getName()); app.setDetailedData(appDetails); } } } } private ImmutableList<String> getNames(Collection<CFApplicationDetail> apps) { ImmutableList.Builder<String> builder = ImmutableList.builder(); for (CFApplicationDetail app : apps) { builder.add(app.getName()); } return builder.build(); } public OperationsExecution getOperationsExecution() { return new OperationsExecution(this); } public void updateApplication(CFApplicationDetail appDetails) { CloudAppDashElement app = getApplication(appDetails.getName()); if (app != null) { app.setDetailedData(appDetails); } } @Override public void delete(Collection<BootDashElement> toRemove, UserInteractions ui) { if (toRemove == null || toRemove.isEmpty()) { return; } List<Mono<Void>> asyncDeletions = new ArrayList<>(toRemove.size()); for (BootDashElement element : toRemove) { if (element instanceof AsyncDeletable) { asyncDeletions.add(((AsyncDeletable) element).deleteAsync()); } else if (element instanceof Deletable) { try { ((Deletable) element).delete(ui); } catch (Exception e) { Log.log(e); } } } if (!asyncDeletions.isEmpty()) { int numElements = asyncDeletions.size(); runAsynch("Deleting [" + numElements + "] services", "", (IProgressMonitor mon) -> { //Careful... deleting more elements takes more time... Duration timeout = Duration.ofSeconds(20 * asyncDeletions.size()); mon.beginTask("Deleting [" + numElements + "] services", numElements); AtomicInteger leftToDelete = new AtomicInteger(numElements); try { ReactorUtils.safeMerge(Flux.fromIterable(asyncDeletions).map((Mono<Void> deleteOp) -> { return deleteOp.doOnTerminate(() -> { mon.worked(1); mon.setTaskName("Deleting [" + leftToDelete.decrementAndGet() + "] services"); }); }), 5 //limit concurrency to avoid flooding/choking request broker ).block(timeout); } finally { mon.done(); } }, ui); } } @Override public String toString() { return this.getClass().getName() + "(" + getRunTarget().getName() + ")"; } @Override public BootDashModelConsoleManager getElementConsoleManager() { return this.consoleManager; } private static ITextFileBuffer getDirtyBuffer(IFile file) { ITextFileBuffer buffer = FileBuffers.getTextFileBufferManager().getTextFileBuffer(file.getFullPath(), LocationKind.IFILE); if (buffer != null && buffer.isDirty()) { return buffer; } return null; } /** * * @param project * @param ui * @param requests * @param monitor * @return non-null deployment properties for the application. * @throws Exception * if error occurred while resolving the deployment properties * @throws OperationCanceledException * if user canceled operation while resolving deployment * properties */ public CloudApplicationDeploymentProperties resolveDeploymentProperties(CloudAppDashElement cde, UserInteractions ui, IProgressMonitor monitor) throws Exception { IProject project = cde.getProject(); CFApplication app = cde.getSummaryData(); CloudData cloudData = buildOperationCloudData(monitor, project); CloudApplicationDeploymentProperties deploymentProperties = CloudApplicationDeploymentProperties .getFor(project, cloudData, app); CloudAppDashElement element = app == null ? null : getApplication(app.getName()); final IFile manifestFile = element == null ? null : element.getDeploymentManifestFile(); if (manifestFile != null) { // Manifest file deployment mode // Check if file exists in case the stored file is obsolete (e.g. no longer exists) if (manifestFile.exists()) { if (!saveManifestBeforePush(manifestFile, ui)) { throw new OperationCanceledException(); } else { final String yamlContents = IOUtil.toString(manifestFile.getContents()); String errorMessage = null; TextEdit edit = null; try { YamlGraphDeploymentProperties yamlGraph = new YamlGraphDeploymentProperties(yamlContents, deploymentProperties.getAppName(), cloudData); MultiTextEdit me = yamlGraph.getDifferences(deploymentProperties); edit = me != null && me.hasChildren() ? me : null; if (yamlGraph.getInheritFilePath() != null) { errorMessage = "'inherit' attribute is present in the manifest but is not supported. Merge with caution."; } } catch (MalformedTreeException e) { Log.log(e); errorMessage = "Failed to create text differences between local manifest file and deployment properties on CF. Merge with caution."; edit = new ReplaceEdit(0, yamlContents.length(), new Yaml(YamlGraphDeploymentProperties.createDumperOptions()) .dump(ApplicationManifestHandler.toYaml(deploymentProperties, cloudData))); } catch (Throwable t) { Log.log(t); errorMessage = "Failed to parse local manifest file YAML contents. Merge with caution."; edit = new ReplaceEdit(0, yamlContents.length(), new Yaml(YamlGraphDeploymentProperties.createDumperOptions()) .dump(ApplicationManifestHandler.toYaml(deploymentProperties, cloudData))); } /* * If UI is available and there differences between manifest and * current deployment properties on CF then prompt the user to * perform the merge */ if (edit != null && ui != null) { final IDocument doc = new Document(yamlContents); edit.apply(doc); final YamlFileInput left = new YamlFileInput(manifestFile, BootDashActivator.getDefault() .getImageRegistry().get(BootDashActivator.CLOUD_ICON)); final YamlInput right = new YamlInput("Current deployment properties from Cloud Foundry", BootDashActivator.getDefault().getImageRegistry().get(BootDashActivator.CLOUD_ICON), doc.get()); CompareConfiguration config = new CompareConfiguration(); config.setLeftLabel(left.getName()); config.setLeftImage(left.getImage()); config.setRightLabel(right.getName()); config.setRightImage(right.getImage()); config.setLeftEditable(true); config.setProperty("manifest", manifestFile); final String message = errorMessage; final CompareEditorInput input = new CompareEditorInput(config) { @Override protected Object prepareInput(IProgressMonitor arg0) throws InvocationTargetException, InterruptedException { setMessage(message); return new DiffNode(left, right); } }; input.setTitle("Merge Local Deployment Manifest File"); input.run(monitor); ManifestDiffDialogModel model = new ManifestDiffDialogModel(input); Result result = ui.openManifestDiffDialog(model); switch (result) { case CANCELED: throw new OperationCanceledException(); case FORGET_MANIFEST: element.setDeploymentManifestFile(null); /* * Use the current CF deployment properties, hence just break out of the switch */ break; case USE_MANIFEST: /* * Load deployment properties from YAML text content */ final byte[] yamlBytes = left.getContent(); List<CloudApplicationDeploymentProperties> props = new ApplicationManifestHandler( project, cloudData, manifestFile) { @Override protected InputStream getInputStream() throws Exception { return new ByteArrayInputStream(yamlBytes); } }.load(monitor); CloudApplicationDeploymentProperties found = null; for (CloudApplicationDeploymentProperties p : props) { if (deploymentProperties.getAppName().equals(p.getAppName())) { found = p; break; } } if (found == null) { throw ExceptionUtil .coreException("Cannot load deployment properties for application '" + deploymentProperties.getAppName() + "' from the manifest file '" + manifestFile.getFullPath() + "'"); } else { deploymentProperties = found; } break; default: } } // TODO: refactor so that adding archive only gets called once for all properties resolving and creating cases. // Reason to call multiple times in different conditions is to retain the old logic when // switching to v2 usage and not introduce regressions with manifest diffing addApplicationArchive(project, deploymentProperties, cloudData, ui, monitor); } } else { // Still in manifest file deployment mode, but manifest file does not exist anymore therefore create properties deploymentProperties = createDeploymentProperties(project, ui, monitor); } } else { // Manual deployment mode addApplicationArchive(project, deploymentProperties, cloudData, ui, monitor); } // TODO: We need to clean up push and restart code. There are multiple paths that end up doing // the same thing, so the check below is appearing in at least two different places. // We should ideally only check for unsupported properties in one place: wherever we resolve // deployment properties regardless of which path we take (either a project deployment or app restart) getUnsupportedProperties().allowOrCancelIfFound(ui, deploymentProperties); return deploymentProperties; } /** * Check for dirty manifest. If manifest is dirty, ask user if it is okay to save. * If they say yes do it. * @return Whether push operation is okay to proceed. (Manifest is not dirty at the end). * @throws ExecutionException * @throws InterruptedException */ private boolean saveManifestBeforePush(IFile manifestFile, UserInteractions ui) throws Exception { ITextFileBuffer dirtyManifest = getDirtyBuffer(manifestFile); if (dirtyManifest == null) { return true; } else { boolean save = ui.confirmOperation( "Save Cf Manifest?", "The CF manifest at `" + manifestFile.getFullPath() + "' has unsaved changes.\n\n" + "Do you want to save it now?", new String[] { "Save", "Cancel" }, 0) == 0; if (save) { JobUtil.runInJob("Save dirty manifest", (progres) -> dirtyManifest.commit(progres, true)).get(); } return save; } } /** * Creates deployment properties either based on user inout via the UI if UI context is available or generates default deployment properties * @param project the workspace project * @param ui UI context * @param monitor progress monitor * @return deployment properties * @throws Exception */ public CloudApplicationDeploymentProperties createDeploymentProperties(IProject project, UserInteractions ui, IProgressMonitor monitor) throws Exception { CloudData cloudData = buildOperationCloudData(monitor, project); CloudApplicationDeploymentProperties props = null; if (ui != null) { DeploymentPropertiesDialogModel dialogModel; dialogModel = new DeploymentPropertiesDialogModel(ui, cloudData, project, null); IFile foundManifestFile = DeploymentPropertiesDialog.findManifestYamlFile(project); dialogModel.setSelectedManifest(foundManifestFile); dialogModel.setManifestType(foundManifestFile == null ? ManifestType.MANUAL : ManifestType.FILE); props = ui.promptApplicationDeploymentProperties(dialogModel); addApplicationArchive(project, props, cloudData, ui, monitor); } return props; } public void addApplicationArchive(IProject project, CloudApplicationDeploymentProperties properties, CloudData cloudData, UserInteractions ui, IProgressMonitor monitor) throws Exception { ICloudApplicationArchiver archiver = getArchiver(properties, cloudData, ui, monitor); if (archiver != null) { File archive = archiver.getApplicationArchive(monitor); properties.setArchive(archive); } else { throw ExceptionUtil.coreException("No applicable archiver strategy found for project '" + project.getName() + "'! " + "Check the project's packaging type; or add " + "an explicit path attribute to your manifest.yml."); } } protected ICloudApplicationArchiver getArchiver(CloudApplicationDeploymentProperties deploymentProperties, CloudData cloudData, UserInteractions ui, IProgressMonitor mon) { try { for (CloudApplicationArchiverStrategy s : getArchiverStrategies(deploymentProperties, cloudData, ui, mon)) { ICloudApplicationArchiver a = s.getArchiver(mon); if (a != null) { return a; } } } catch (Exception e) { Log.log(e); } return null; } protected CloudApplicationArchiverStrategy[] getArchiverStrategies( CloudApplicationDeploymentProperties deploymentProperties, CloudData cloudData, UserInteractions ui, IProgressMonitor mon) throws Exception { IProject project = deploymentProperties.getProject(); IFile manifestFile = deploymentProperties.getManifestFile(); String appName = deploymentProperties.getAppName(); ApplicationManifestHandler parser = new ApplicationManifestHandler(project, cloudData, manifestFile); return new CloudApplicationArchiverStrategy[] { CloudApplicationArchiverStrategies.fromManifest(project, appName, parser), CloudApplicationArchiverStrategies.packageAsJar(project, ui), CloudApplicationArchiverStrategies.packageMvnAsWar(project, ui) }; } @Override public boolean canDelete(BootDashElement element) { return element instanceof Deletable || element instanceof AsyncDeletable; } @Override public String getDeletionConfirmationMessage(Collection<BootDashElement> value) { return "Are you sure that you want to delete the selected applications/services from: " + getRunTarget().getName() + "? The applications/services will be permanently removed."; } public boolean isConnected() { return getRunTarget().isConnected(); } public void setServices(Set<CloudServiceInstanceDashElement> newServices) { this.services.replaceAll(newServices); } public ObservableSet<CloudAppDashElement> getApplications() { return applications.getApplications(); } public ImmutableSet<CloudAppDashElement> getApplicationValues() { return applications.getApplicationValues(); } public ObservableSet<CloudServiceInstanceDashElement> getServices() { return services; } public CloudData buildOperationCloudData(IProgressMonitor monitor, IProject project) throws Exception { return new CloudData(getRunTarget().getDomains(monitor), getRunTarget().getBuildpack(project), getRunTarget().getStacks(monitor)); } public CloudDashElementFactory getElementFactory() { return elementFactory; } public IPropertyStore getPropertyStore() { return modelStore; } public void disconnect() { getRunTarget().disconnect(); } public void connect() throws Exception { getRunTarget().connect(); apiWarning.refresh(); } public ClientRequests getClient() { return getRunTarget().getClient(); } private LiveExpression<ClientRequests> getClientExp() { return getRunTarget().getClientExp(); } public List<CFCloudDomain> getCloudDomains(IProgressMonitor monitor) throws Exception { return getRunTarget().getDomains(monitor); } /* TODO: These asynch methods probably should not be here but leaving them in the model for now as model is commonly shared across boot dash */ public void runAsynch(String opName, String appName, JobBody body, UserInteractions ui) { getOperationsExecution().runAsynch(opName, appName, body, ui); } public void runSynch(String opName, String appName, JobBody body, UserInteractions ui) throws Exception { CompletableFuture<Void> f = new CompletableFuture<>(); runAsynch(opName, appName, (mon) -> { try { body.run(mon); f.complete(null); } catch (Throwable e) { f.completeExceptionally(e); } }, ui); f.get(); } public void runAsynch(Operation<?> op, UserInteractions ui) { getOperationsExecution().runAsynch(op, ui); } public void removeService(String serviceName) { for (CloudServiceInstanceDashElement s : services.getValues()) { if (s.getName().equals(serviceName)) { services.remove(s); } } } public void setBaseRefreshState(RefreshState newState) { baseRefeshState.setValue(newState); } public UnsupportedPushProperties getUnsupportedProperties() { return unsupportedPushProperties; } }