/******************************************************************************* * Copyright (c) 2015, 2017 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 * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.dash.model; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jdt.core.IJavaProject; import org.osgi.framework.Bundle; import org.osgi.framework.BundleEvent; import org.osgi.framework.BundleListener; import org.springframework.ide.eclipse.boot.core.cli.BootCliUtils; import org.springframework.ide.eclipse.boot.core.cli.BootInstallManager; import org.springframework.ide.eclipse.boot.core.cli.BootInstallManager.BootInstallListener; import org.springframework.ide.eclipse.boot.core.cli.CloudCliUtils; import org.springframework.ide.eclipse.boot.core.cli.install.IBootInstall; import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsPortRefresher; import org.springframework.ide.eclipse.boot.dash.livexp.LiveSets; import org.springframework.ide.eclipse.boot.dash.metadata.IPropertyStore; import org.springframework.ide.eclipse.boot.dash.metadata.PropertyStoreFactory; import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetType; import org.springframework.ide.eclipse.boot.dash.util.LaunchConfRunStateTracker; import org.springframework.ide.eclipse.boot.dash.util.LaunchConfigurationTracker; import org.springframework.ide.eclipse.boot.dash.views.BootDashModelConsoleManager; import org.springframework.ide.eclipse.boot.dash.views.BootDashTreeView; import org.springframework.ide.eclipse.boot.dash.views.LocalElementConsoleManager; import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; import org.springframework.ide.eclipse.boot.util.Log; import org.springsource.ide.eclipse.commons.frameworks.core.workspace.ClasspathListenerManager; import org.springsource.ide.eclipse.commons.frameworks.core.workspace.ClasspathListenerManager.ClasspathListener; import org.springsource.ide.eclipse.commons.frameworks.core.workspace.ProjectChangeListenerManager; import org.springsource.ide.eclipse.commons.frameworks.core.workspace.ProjectChangeListenerManager.ProjectChangeListener; import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; 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; /** * Model of the contents for {@link BootDashTreeView}, provides mechanism to attach listeners to model * and attaches itself as a workspace listener to keep the model in synch with workspace changes. * * @author Kris De Volder */ public class LocalBootDashModel extends AbstractBootDashModel implements DeletionCapabableModel { private IWorkspace workspace; private BootProjectDashElementFactory projectElementFactory; private LaunchConfDashElementFactory launchConfElementFactory; ProjectChangeListenerManager openCloseListenerManager; ClasspathListenerManager classpathListenerManager; private final LaunchConfRunStateTracker launchConfRunStateTracker = new LaunchConfRunStateTracker(); final LaunchConfigurationTracker launchConfTracker = new LaunchConfigurationTracker(BootLaunchConfigurationDelegate.TYPE_ID); private LiveSetVariable<BootProjectDashElement> applications; //lazy created private LiveSetVariable<LocalCloudServiceDashElement> cloudCliservices; private ObservableSet<BootDashElement> allElements; private BootDashModelConsoleManager consoleManager; private DevtoolsPortRefresher devtoolsPortRefresher; private LiveExpression<Pattern> projectExclusion; private ValueListener<Pattern> projectExclusionListener; private BootInstallListener bootInstallListener; private IPropertyStore modelStore; private LiveVariable<RefreshState> bootAppsRefreshState = new LiveVariable<>(RefreshState.READY); private LiveVariable<RefreshState> cloudCliServicesRefreshState = new LiveVariable<>(RefreshState.READY); private LiveExpression<Boolean> hideCloudCliServices = new LiveExpression<Boolean>() { { dependsOn(getViewModel().getToggleFilters().getSelectedFilters()); } @Override protected Boolean compute() { return getViewModel().getToggleFilters().getSelectedFilters() .contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES); } }; private LiveExpression<RefreshState> refreshState = new LiveExpression<RefreshState>() { { dependsOn(bootAppsRefreshState); dependsOn(cloudCliServicesRefreshState); addListener((e,v) -> notifyModelStateChanged()); } @Override protected RefreshState compute() { return RefreshState.merge(bootAppsRefreshState.getValue(), cloudCliServicesRefreshState.getValue()); } }; public class WorkspaceListener implements ProjectChangeListener, ClasspathListener { @Override public void projectChanged(IProject project) { updateElementsFromWorkspace(); } @Override public void classpathChanged(IJavaProject jp) { updateElementsFromWorkspace(); } } public LocalBootDashModel(BootDashModelContext context, BootDashViewModel parent) { super(RunTargets.LOCAL, parent); this.workspace = context.getWorkspace(); this.launchConfElementFactory = new LaunchConfDashElementFactory(this, context.getLaunchManager()); this.projectElementFactory = new BootProjectDashElementFactory(this, context.getProjectProperties(), launchConfElementFactory); this.consoleManager = new LocalElementConsoleManager(); this.projectExclusion = context.getBootProjectExclusion(); RunTargetType type = getRunTarget().getType(); IPropertyStore typeStore = PropertyStoreFactory.createForScope(type, context.getRunTargetProperties()); this.modelStore = PropertyStoreFactory.createSubStore(getRunTarget().getId(), typeStore); // Listen to M2E JDT plugin active event to refresh local boot project dash elements. addMavenInitializationIssueEventHandling(); } /** * Refresh boot project dash elements once m2e JDT plugin is fully * initialized. Boot project checks may not succeed in some cases if m2e JDT * hasn't completed it's start procedure */ private void addMavenInitializationIssueEventHandling() { Bundle bundle = Platform.getBundle("org.eclipse.m2e.jdt"); if (bundle != null) { BundleListener listener = new BundleListener() { @Override public void bundleChanged(BundleEvent event) { if (event.getBundle() == bundle && event.getType() == BundleEvent.STARTED) { try { updateElementsFromWorkspace(); } catch (Throwable t) { Log.log(t); } finally { bundle.getBundleContext().removeBundleListener(this); } } } }; bundle.getBundleContext().addBundleListener(listener); } } void init() { if (allElements==null) { this.applications = new LiveSetVariable<>(AsyncMode.SYNC); this.cloudCliservices = new LiveSetVariable<>(AsyncMode.SYNC); this.allElements = LiveSets.union(this.applications, this.cloudCliservices); WorkspaceListener workspaceListener = new WorkspaceListener(); this.openCloseListenerManager = new ProjectChangeListenerManager(workspace, workspaceListener); this.classpathListenerManager = new ClasspathListenerManager(workspaceListener); projectExclusion.addListener(projectExclusionListener = new ValueListener<Pattern>() { public void gotValue(LiveExpression<Pattern> exp, Pattern value) { updateElementsFromWorkspace(); } }); refresh(null); bootInstallListener = new BootInstallListener() { @Override public void defaultInstallChanged() { refreshLocalCloudServices(); } }; try { BootInstallManager.getInstance().addBootInstallListener(bootInstallListener); } catch (Exception e) { Log.log(e); } // Listen to changes in "Hide Local Cloud Services" filter toggle hideCloudCliServices.addListener((e, v) -> refreshLocalCloudServices()); this.devtoolsPortRefresher = new DevtoolsPortRefresher(this, projectElementFactory); } } /** * When no longer needed the model should be disposed, otherwise it will continue * listening for changes to the workspace in order to keep itself in synch. */ public void dispose() { if (applications!=null) { applications.getValue().forEach(bde -> bde.dispose()); applications.dispose(); applications = null; openCloseListenerManager.dispose(); openCloseListenerManager = null; classpathListenerManager.dispose(); classpathListenerManager = null; devtoolsPortRefresher.dispose(); devtoolsPortRefresher = null; } if (launchConfElementFactory!=null) { launchConfElementFactory.dispose(); launchConfElementFactory = null; } if (projectElementFactory!=null) { projectElementFactory.dispose(); projectElementFactory = null; } if (projectExclusionListener!=null) { projectExclusion.removeListener(projectExclusionListener); projectExclusionListener=null; } if (bootInstallListener != null) { try { BootInstallManager.getInstance().removeBootInstallListener(bootInstallListener); } catch (Exception e) { Log.log(e); } } if (cloudCliservices != null) { cloudCliservices.getValue().forEach(bde -> bde.dispose()); cloudCliservices.dispose(); cloudCliservices = null; } if (allElements != null) { allElements.dispose(); allElements = null; } hideCloudCliServices.dispose(); launchConfTracker.dispose(); launchConfRunStateTracker.dispose(); } void updateElementsFromWorkspace() { Set<BootProjectDashElement> newElements = Arrays.stream(this.workspace.getRoot().getProjects()) .map(projectElementFactory::createOrGet) .filter(Objects::nonNull) .collect(Collectors.toSet()); applications.replaceAll(newElements); projectElementFactory.disposeAllExcept(newElements); } public synchronized ObservableSet<BootDashElement> getElements() { init(); return allElements; } /** * Trigger manual model refresh. */ public void refresh(UserInteractions ui) { updateElementsFromWorkspace(); refreshLocalCloudServices(); } private List<LocalCloudServiceDashElement> fetchLocalServices() { try { IBootInstall bootInstall = BootCliUtils.getSpringBootInstall(); try { return Arrays.stream(CloudCliUtils.getCloudServices(bootInstall)).map(serviceId -> new LocalCloudServiceDashElement(this, serviceId)).collect(Collectors.toList()); } catch (CoreException e) { // Core Exception would be thrown if Spring Cloud CLI command fails to execute Log.log(e); } } catch (Exception e) { // ignore } return Collections.emptyList(); } private void refreshLocalCloudServices() { if (hideCloudCliServices.getValue()) { cloudCliservices.getValue().forEach(bde -> bde.dispose()); cloudCliservices.replaceAll(Collections.emptySet()); } else { new Job("Loading local cloud services") { @Override protected IStatus run(IProgressMonitor monitor) { try { cloudCliServicesRefreshState.setValue(RefreshState.loading("Fetching Local Cloud Sevices...")); List<LocalCloudServiceDashElement> newCloudCliservices = fetchLocalServices(); cloudCliservices.getValue().forEach(bde -> bde.dispose()); cloudCliservices.replaceAll(newCloudCliservices); return Status.OK_STATUS; } finally { cloudCliServicesRefreshState.setValue(RefreshState.READY); } } }.schedule(); } } @Override public BootDashModelConsoleManager getElementConsoleManager() { return consoleManager; } public LaunchConfRunStateTracker getLaunchConfRunStateTracker() { return launchConfRunStateTracker; } public BootProjectDashElementFactory getProjectElementFactory() { return projectElementFactory; } public LaunchConfDashElementFactory getLaunchConfElementFactory() { return launchConfElementFactory; } @Override public void delete(Collection<BootDashElement> elements, UserInteractions ui) { for (BootDashElement e : elements) { if (e instanceof Deletable) { ((Deletable)e).delete(ui); } } } @Override public boolean canDelete(BootDashElement element) { return element instanceof Deletable; } @Override public String getDeletionConfirmationMessage(Collection<BootDashElement> value) { return "Are you sure you want to delete the selected local launch configuration(s)? The configuration(s) will be permanently removed from the workspace."; } public IPropertyStore getModelStore() { return modelStore; } @Override public RefreshState getRefreshState() { return refreshState.getValue(); } }