/*******************************************************************************
* Copyright (c) 2015, 2016 Pivotal Software, 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 Software, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.ui;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.ui.IObjectActionDelegate;
import org.eclipse.ui.IWorkbenchPart;
import org.osgi.framework.Version;
import org.osgi.framework.VersionRange;
import org.springframework.ide.eclipse.boot.core.BootActivator;
import org.springframework.ide.eclipse.boot.core.BootPropertyTester;
import org.springframework.ide.eclipse.boot.core.IMavenCoordinates;
import org.springframework.ide.eclipse.boot.core.ISpringBootProject;
import org.springframework.ide.eclipse.boot.core.MavenCoordinates;
import org.springframework.ide.eclipse.boot.core.SpringBootCore;
import org.springframework.ide.eclipse.boot.core.SpringBootStarter;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springframework.ide.eclipse.core.SpringCore;
import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil;
public class EnableDisableBootDevtools implements IObjectActionDelegate {
private static final VersionRange DEVTOOLS_SUPPORTED = new VersionRange("1.3.0");
public static final String SPRING_BOOT_DEVTOOLS_AID = "spring-boot-devtools";
public static final String SPRING_BOOT_DEVTOOLS_GID = "org.springframework.boot";
private static final SpringBootStarter DEVTOOLS_STARTER = new SpringBootStarter("devtools",
new MavenCoordinates(SPRING_BOOT_DEVTOOLS_GID, SPRING_BOOT_DEVTOOLS_AID, null),
"compile", /*bom*/null, /*repo*/null
);
private SpringBootCore springBootCore;
private IProject project;
private IWorkbenchPart activePart;
private ISpringBootProject bootProject;
/**
* Constructor that eclipse calls when it instantiates the delegate
*/
public EnableDisableBootDevtools() {
this(SpringBootCore.getDefault());
}
/**
* Constructor that test code can use to inject mocks etc.
*/
public EnableDisableBootDevtools(SpringBootCore springBootCore) {
this.springBootCore = springBootCore;
}
@Override
public void run(IAction action) {
try {
SpringBootStarter devtools = getAvaibleDevtools(bootProject);
if (hasDevTools(bootProject)) {
bootProject.removeMavenDependency(devtools.getMavenId());
} else {
if (devtools!=null) {
bootProject.addMavenDependency(devtools.getDependency(), /*preferManaged*/true);
} else {
MessageDialog.openError(activePart.getSite().getShell(), "Boot Devtools Dependency could not be added", explainFailure());
}
}
} catch (Exception e) {
BootActivator.log(e);
MessageDialog.openError(activePart.getSite().getShell(), "Unexpected failure",
"The action to add/remove devtools unexpectedly failed with an error:\n" +
ExceptionUtil.getMessage(e) + "\n" +
"The error log may contain further information.");
}
}
private String explainFailure() throws Exception {
if (project==null) {
return "No project selected";
} else if (!BootPropertyTester.isBootProject(project)) {
return "Project '"+project.getProject().getName()+"' does not seem to be a Spring Boot project";
} else if (!project.hasNature(SpringBootCore.M2E_NATURE)) {
return "Project '"+project.getProject().getName()+"' is not an Maven/m2e enabled project. This action's implementation requires m2e to add/remove "
+ "the Devtools as a dependency to your project.";
} else {
String version = bootProject.getBootVersion();
return "Boot Devtools are provided by Spring Boot version 1.3.0 or later. "
+ "Project '"+project.getProject().getName()+"' uses Boot Version "+version;
}
}
@Override
public void selectionChanged(IAction action, ISelection selection) {
try {
project = getProject(selection);
bootProject = getBootProject(project);
} catch (Exception e) {
BootActivator.log(e);
}
action.setEnabled(project!=null);
if (bootProject!=null) {
try {
action.setText(fastHasDevTools(bootProject)?"Remove Boot Devtools":"Add Boot Devtools");
} catch (TimeoutException | InterruptedException e) {
action.setText("Add/Remove Boot Devtools");
} catch (Exception e) {
//Unexpected
Log.log(e);
}
} else if (project!=null) {
//action shouldn't really be enabled, but it is enabled so that it can
// fail with an explanation when the user tries it.
action.setText("Add/Remove Boot Devtools");
}
}
/**
* Like hasDevTools, but suitable for calling on the UI thread. This operation may fail
* with a {@link TimeoutException} if it can not be readily determined whether a project
* has dev tools as a dependency (this may happen, for example because m2e is still in the process of
* resolving the dependencies). It would be undesirable to block on the UI thread to wait for this
* process to complete.
*/
private boolean fastHasDevTools(ISpringBootProject bootProject) throws TimeoutException, InterruptedException, ExecutionException {
CompletableFuture<Boolean> result = new CompletableFuture<>();
Job job = new Job("Check for devtools") {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
result.complete(hasDevTools(bootProject));
} catch (Throwable e) {
result.completeExceptionally(e);
}
return Status.OK_STATUS;
}
};
job.setSystem(true);
job.schedule();
return result.get(100, TimeUnit.MILLISECONDS);
}
private boolean hasDevTools(ISpringBootProject bootProject) {
try {
List<IMavenCoordinates> deps = bootProject.getDependencies();
if (deps!=null) {
for (IMavenCoordinates d : deps) {
if (SPRING_BOOT_DEVTOOLS_AID.equals(d.getArtifactId())) {
return true;
}
}
}
} catch (Exception e) {
BootActivator.log(e);
}
return false;
}
private SpringBootStarter getAvaibleDevtools(ISpringBootProject project) {
try {
String versionString = project.getBootVersion();
if (StringUtils.isNotBlank(versionString) && DEVTOOLS_SUPPORTED.includes(new Version(versionString))) {
return DEVTOOLS_STARTER;
}
} catch (Exception e) {
BootActivator.log(e);
}
return null;
}
private IProject getProject(ISelection selection) {
try {
if (selection instanceof IStructuredSelection) {
IStructuredSelection ss = (IStructuredSelection) selection;
if (ss.size()==1) {
Object el = ss.getFirstElement();
if (el instanceof IProject) {
IProject p = (IProject) el;
if (p.isAccessible() && p.hasNature(SpringCore.NATURE_ID)) {
//only interested in spring projects.
return p;
}
}
}
}
} catch (Exception e) {
BootActivator.log(e);
}
return null;
}
private ISpringBootProject getBootProject(IProject project) {
try {
if (project!=null) {
return springBootCore.project(project);
}
} catch (Exception e) {
if (!isExpected(e)) {
BootActivator.log(e);
}
}
return null;
}
private boolean isExpected(Exception e) {
//See https://issuetracker.springsource.com/browse/STS-4263
String msg = ExceptionUtil.getMessage(e);
return msg!=null && msg.contains("only implemented for m2e");
}
@Override
public void setActivePart(IAction action, IWorkbenchPart targetPart) {
activePart = targetPart;
}
}