diff --git a/eclipse-distribution/org.springframework.boot.ide.product.e416/category.xml b/eclipse-distribution/org.springframework.boot.ide.product.e416/category.xml index 595914bb8..9cd12db42 100644 --- a/eclipse-distribution/org.springframework.boot.ide.product.e416/category.xml +++ b/eclipse-distribution/org.springframework.boot.ide.product.e416/category.xml @@ -39,16 +39,6 @@ - - - - - - - - diff --git a/eclipse-distribution/org.springframework.boot.ide.product.e417/category.xml b/eclipse-distribution/org.springframework.boot.ide.product.e417/category.xml index 42e9381c0..eeaa9ce8d 100644 --- a/eclipse-distribution/org.springframework.boot.ide.product.e417/category.xml +++ b/eclipse-distribution/org.springframework.boot.ide.product.e417/category.xml @@ -39,16 +39,6 @@ - - - - - - - - diff --git a/eclipse-distribution/org.springframework.boot.ide.product.e418/category.xml b/eclipse-distribution/org.springframework.boot.ide.product.e418/category.xml index 42e9381c0..eeaa9ce8d 100644 --- a/eclipse-distribution/org.springframework.boot.ide.product.e418/category.xml +++ b/eclipse-distribution/org.springframework.boot.ide.product.e418/category.xml @@ -39,16 +39,6 @@ - - - - - - - - diff --git a/eclipse-distribution/pom.xml b/eclipse-distribution/pom.xml index 7ada1124f..25c45da73 100644 --- a/eclipse-distribution/pom.xml +++ b/eclipse-distribution/pom.xml @@ -43,6 +43,34 @@ org.springframework.boot.ide.branding org.springframework.boot.ide.branding.feature + + ../eclipse-extensions/org.springframework.ide.eclipse.boot + ../eclipse-extensions/org.springframework.ide.eclipse.buildship30 + ../eclipse-extensions/org.springframework.ide.eclipse.imports + ../eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live + ../eclipse-extensions/org.springframework.ide.eclipse.boot.dash + ../eclipse-extensions/org.springframework.ide.eclipse.boot.launch + ../eclipse-extensions/org.springframework.ide.eclipse.boot.refactoring + ../eclipse-extensions/org.springframework.ide.eclipse.boot.restart + ../eclipse-extensions/org.springframework.ide.eclipse.boot.templates + ../eclipse-extensions/org.springframework.ide.eclipse.boot.test + ../eclipse-extensions/org.springframework.ide.eclipse.boot.validation + ../eclipse-extensions/org.springframework.ide.eclipse.boot.wizard + ../eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure + ../eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf + ../eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker + ../eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature + ../eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test + ../eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test + ../eclipse-extensions/org.springframework.ide.eclipse.boot.refactoring.test + ../eclipse-extensions/org.springframework.ide.eclipse.docker.client + ../eclipse-extensions/org.springframework.ide.eclipse.editor.support + ../eclipse-extensions/org.springframework.ide.eclipse.maven.pom + ../eclipse-extensions/org.springframework.ide.eclipse.maven.pom.tests + ../eclipse-extensions/org.springframework.ide.eclipse.xml.namespaces + ../eclipse-extensions/org.springframework.ide.eclipse.xml.namespaces.feature + ../eclipse-extensions/org.springsource.ide.eclipse.commons.cloudfoundry.client.v2 + @@ -69,17 +97,12 @@ nightly ${dist.type}/${dist.key}/${dist.project}/${dist.pathpostfix} - https://dist.springsource.com/snapshot/TOOLS/eclipse-integration-commons/nightly - https://dist.springframework.org/snapshot/IDE/nightly - - - https://dist.springsource.com/${dist.type}/TOOLS/sts4-language-server-integrations/${sts4-language-servers-version} 1.7.0 UTF-8 + + 3.9.4.202003091750 @@ -458,14 +481,14 @@ ${sts4-language-servers-p2-repo} - springide + yedit p2 - ${springide-p2-repo} + https://dist.springsource.com/release/TOOLS/third-party/yedit - eclipse-integration-commons + p2-thirdparty-bundles p2 - ${eclipsecommons-p2-repo} + https://dist.springsource.com/release/TOOLS/third-party/misc-p2-repo/${misc.p2.repo.version} macosx diff --git a/eclipse-extensions/org.springframework.boot.ide.main.feature/pom.xml b/eclipse-extensions/org.springframework.boot.ide.main.feature/pom.xml index d4e90f252..cf4cc29b8 100644 --- a/eclipse-extensions/org.springframework.boot.ide.main.feature/pom.xml +++ b/eclipse-extensions/org.springframework.boot.ide.main.feature/pom.xml @@ -2,6 +2,7 @@ 4.0.0 + org.springframework.boot.ide org.springframework.boot.ide diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/.classpath new file mode 100644 index 000000000..eca7bdba8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/.project b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/.project new file mode 100644 index 000000000..cbbc2d3cb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/.project @@ -0,0 +1,28 @@ + + + org.springframework.ide.eclipse.beans.ui.live + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..0c68a61dc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/META-INF/MANIFEST.MF b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/META-INF/MANIFEST.MF new file mode 100644 index 000000000..f59487976 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/META-INF/MANIFEST.MF @@ -0,0 +1,27 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Live Beans Tree +Bundle-SymbolicName: org.springframework.ide.eclipse.beans.ui.live;singleton:=true +Bundle-Version: 4.8.1.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Export-Package: org.springframework.ide.eclipse.beans.ui.live, + org.springframework.ide.eclipse.beans.ui.live.actions, + org.springframework.ide.eclipse.beans.ui.live.model, + org.springframework.ide.eclipse.beans.ui.live.tree, + org.springframework.ide.eclipse.beans.ui.live.utils +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.ui.views, + org.json, + org.eclipse.core.resources, + org.apache.commons.lang, + org.eclipse.jface, + org.eclipse.ui.workbench, + org.eclipse.jdt.core, + org.springsource.ide.eclipse.commons.ui, + org.eclipse.jdt.ui, + com.google.guava, + org.springsource.ide.eclipse.commons.livexp, + org.springsource.ide.eclipse.commons.frameworks.core +Bundle-ActivationPolicy: lazy +Bundle-Vendor: Spring IDE Developers +Bundle-Activator: org.springframework.ide.eclipse.beans.ui.live.LiveBeansUiPlugin diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/build.properties b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/build.properties new file mode 100644 index 000000000..c6baffa00 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/build.properties @@ -0,0 +1,5 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + icons/ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/bean_obj.gif b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/bean_obj.gif new file mode 100644 index 000000000..e89df3c62 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/bean_obj.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/beanref_obj.gif b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/beanref_obj.gif new file mode 100755 index 000000000..515506901 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/beanref_obj.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/collection_obj.gif b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/collection_obj.gif new file mode 100755 index 000000000..ed38f2378 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/collection_obj.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/config_obj.gif b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/config_obj.gif new file mode 100644 index 000000000..507b8a82e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/icons/config_obj.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/pom.xml new file mode 100644 index 000000000..409484fb3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + org.springframework.boot.ide + org.springframework.boot.ide + 4.8.1-SNAPSHOT + ../../eclipse-distribution/pom.xml + + + org.springframework.ide.eclipse + org.springframework.ide.eclipse.beans.ui.live + + eclipse-plugin + + + + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + p2 + ignore + + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + second-generate-p2-metadata + + p2-metadata + + verify + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/LiveBeansUiPlugin.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/LiveBeansUiPlugin.java new file mode 100644 index 000000000..4fa7897fd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/LiveBeansUiPlugin.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live; + +import org.eclipse.jface.resource.ImageRegistry; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +/** + * Live Beans plugin + * + * @author Alex Boyko + * + */ +public class LiveBeansUiPlugin extends AbstractUIPlugin { + + // The plug-in ID + public static final String PLUGIN_ID = "org.springframework.ide.eclipse.beans.ui.live"; //$NON-NLS-1$ + + private static final String ICON_PATH_PREFIX = "icons/"; + private static final String NAME_PREFIX = PLUGIN_ID + '.'; + private static final int NAME_PREFIX_LENGTH = NAME_PREFIX.length(); + + public static final String IMG_OBJS_BEAN = NAME_PREFIX + "bean_obj.gif"; + public static final String IMG_OBJS_BEAN_REF = NAME_PREFIX + "beanref_obj.gif"; + public static final String IMG_OBJS_CONFIG = NAME_PREFIX + "config_obj.gif"; + public static final String IMG_OBJS_COLLECTION = NAME_PREFIX + "collection_obj.gif"; + + private static LiveBeansUiPlugin plugin; + + + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + plugin = this; + } + + @Override + public void stop(BundleContext context) throws Exception { + plugin = null; + super.stop(context); + } + + public static LiveBeansUiPlugin getDefault() { + return plugin; + } + + @Override + protected void initializeImageRegistry(ImageRegistry reg) { + super.initializeImageRegistry(reg); + reg.put(IMG_OBJS_BEAN, imageDescriptorFromPlugin(PLUGIN_ID, ICON_PATH_PREFIX + IMG_OBJS_BEAN.substring(NAME_PREFIX_LENGTH))); + reg.put(IMG_OBJS_BEAN_REF, imageDescriptorFromPlugin(PLUGIN_ID, ICON_PATH_PREFIX + IMG_OBJS_BEAN_REF.substring(NAME_PREFIX_LENGTH))); + reg.put(IMG_OBJS_CONFIG, imageDescriptorFromPlugin(PLUGIN_ID, ICON_PATH_PREFIX + IMG_OBJS_CONFIG.substring(NAME_PREFIX_LENGTH))); + reg.put(IMG_OBJS_COLLECTION, imageDescriptorFromPlugin(PLUGIN_ID, ICON_PATH_PREFIX + IMG_OBJS_COLLECTION.substring(NAME_PREFIX_LENGTH))); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/actions/AbstractOpenResourceAction.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/actions/AbstractOpenResourceAction.java new file mode 100644 index 000000000..e76d89550 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/actions/AbstractOpenResourceAction.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2013, 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.beans.ui.live.actions; + +import org.eclipse.ui.actions.BaseSelectionListenerAction; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public abstract class AbstractOpenResourceAction extends BaseSelectionListenerAction { + + protected AbstractOpenResourceAction(String text) { + super(text); + } + + protected boolean hasTypeInProject(TypeLookup workspaceContext, String className) { + return workspaceContext.findType(className) != null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/actions/OpenBeanClassAction.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/actions/OpenBeanClassAction.java new file mode 100644 index 000000000..99b63d2b4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/actions/OpenBeanClassAction.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2012, 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.beans.ui.live.actions; + +import java.util.List; + +import org.eclipse.jface.viewers.IStructuredSelection; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBean; +import org.springframework.ide.eclipse.beans.ui.live.utils.LiveBeanUtil; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class OpenBeanClassAction extends AbstractOpenResourceAction { + + public OpenBeanClassAction() { + super("Open Bean Class"); + } + + @Override + public void run() { + IStructuredSelection selection = getStructuredSelection(); + List elements = selection.toList(); + for (Object obj : elements) { + if (obj instanceof LiveBean) { + LiveBean bean = (LiveBean) obj; + LiveBeanUtil.navigateToType(bean); + } + } + } + + @Override + protected boolean updateSelection(IStructuredSelection selection) { + if (!selection.isEmpty()) { + List elements = selection.toList(); + for (Object obj : elements) { + if (obj instanceof LiveBean) { + LiveBean bean = (LiveBean) obj; + String beanClass = bean.getBeanType(); + if (beanClass != null && beanClass.trim().length() > 0) { + return true; + } + else { + return hasTypeInProject(bean.getTypeLookup(), bean.getId()); + } + } + } + } + return false; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/actions/OpenBeanDefinitionAction.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/actions/OpenBeanDefinitionAction.java new file mode 100644 index 000000000..9ca43cb74 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/actions/OpenBeanDefinitionAction.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2012, 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.beans.ui.live.actions; + +import java.util.List; + +import org.eclipse.jface.viewers.IStructuredSelection; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBean; +import org.springframework.ide.eclipse.beans.ui.live.utils.LiveBeanUtil; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class OpenBeanDefinitionAction extends AbstractOpenResourceAction { + + public OpenBeanDefinitionAction() { + super("Open Bean Definition File"); + } + + @Override + public void run() { + IStructuredSelection selection = getStructuredSelection(); + List elements = selection.toList(); + for (Object obj : elements) { + if (obj instanceof LiveBean) { + LiveBean bean = (LiveBean) obj; + LiveBeanUtil.navigateToResource(bean); + } + } + } + + @Override + protected boolean updateSelection(IStructuredSelection selection) { + if (!selection.isEmpty()) { + List elements = selection.toList(); + for (Object obj : elements) { + if (obj instanceof LiveBean) { + LiveBean bean = (LiveBean) obj; + String resource = bean.getResource(); + if (resource != null && resource.trim().length() > 0 && !resource.equalsIgnoreCase("null")) { + return true; + } + } + } + } + return false; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/AbstractLiveBeansModelElement.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/AbstractLiveBeansModelElement.java new file mode 100644 index 000000000..cc29fd559 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/AbstractLiveBeansModelElement.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.ui.views.properties.IPropertySource; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public abstract class AbstractLiveBeansModelElement implements IAdaptable, DisplayName { + + protected final Map attributes; + + public AbstractLiveBeansModelElement() { + this.attributes = new HashMap(); + } + + public void addAttribute(String key, String value) { + attributes.put(key, value); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Object getAdapter(Class adapter) { + if (adapter == IPropertySource.class) { + return new LiveBeanPropertySource(this); + } + return null; + } + + public Map getAttributes() { + return attributes; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/DisplayName.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/DisplayName.java new file mode 100644 index 000000000..1406b0314 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/DisplayName.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +/** + * Mark UI elements which are able to provide their label + * + * @author Alex Boyko + * + */ +public interface DisplayName { + + String getDisplayName(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/JsonParser.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/JsonParser.java new file mode 100644 index 000000000..79045263a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/JsonParser.java @@ -0,0 +1,17 @@ +/******************************************************************************* + * Copyright (c) 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.beans.ui.live.model; + +public interface JsonParser { + + T parse(String jsonInput) throws Exception; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBean.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBean.java new file mode 100644 index 000000000..54d92e4be --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBean.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class LiveBean extends AbstractLiveBeansModelElement { + + public static final String ATTR_BEAN = "bean"; + + public static final String ATTR_SCOPE = "scope"; + + public static final String ATTR_TYPE = "type"; + + public static final String ATTR_RESOURCE = "resource"; + + public static final String ATTR_DEPENDENCIES = "dependencies"; + + public static final String ATTR_APPLICATION = "application name"; + + private final TypeLookup typeLookup; + + private final String beanId; + + private String displayName; + + private final Set dependencies; + + private final Set injectedInto; + + private final boolean innerBean; + + public LiveBean(TypeLookup session, String id) { + this(session, id, false); + } + + public LiveBean(TypeLookup typeLookup, String id, boolean innerBean) { + this.typeLookup = typeLookup; + this.beanId = id; + this.innerBean = innerBean; + dependencies = new LinkedHashSet(); + injectedInto = new LinkedHashSet(); + attributes.put(ATTR_BEAN, id); + } + + public void addDependency(LiveBean dependency) { + dependencies.add(dependency); + dependency.injectInto(this); + } + + public String getApplicationName() { + return attributes.get(ATTR_APPLICATION); + } + + public String getBeanType() { + return attributes.get(ATTR_TYPE); + } + + public Set getDependencies() { + return dependencies; + } + + public String getDisplayName() { + // compute the display name the first time it's needed + if (displayName == null) { + // truncate Class names and name with multiple segments + if ((getBeanType() != null && beanId.contains(getBeanType())) || StringUtils.countMatches(beanId, ".") > 1) { + int index = beanId.lastIndexOf('.'); + if (index >= 0) { + displayName = beanId.substring(index + 1, beanId.length()); + } + } + if (displayName == null) { + displayName = beanId; + } + } + return displayName; + } + + public String getId() { + return beanId; + } + + public Set getInjectedInto() { + return injectedInto; + } + + public String getResource() { + return attributes.get(ATTR_RESOURCE); + } + + public String getScope() { + return attributes.get(ATTR_SCOPE); + } + + private void injectInto(LiveBean bean) { + injectedInto.add(bean); + } + + public boolean isInnerBean() { + return innerBean; + } + + public TypeLookup getTypeLookup() { + return typeLookup; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof LiveBean) { + LiveBean other = (LiveBean) obj; + return this.attributes.equals(other.attributes); + } + return super.equals(obj); + } + + @Override + public int hashCode() { + return beanId == null ? super.hashCode() : beanId.hashCode(); + } + + @Override + public String toString() { + return "LiveBean("+getId()+")"; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeanPropertySource.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeanPropertySource.java new file mode 100644 index 000000000..f1dd330e8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeanPropertySource.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.ui.views.properties.IPropertyDescriptor; +import org.eclipse.ui.views.properties.IPropertySource; +import org.eclipse.ui.views.properties.PropertyDescriptor; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class LiveBeanPropertySource implements IPropertySource { + + private final AbstractLiveBeansModelElement model; + + private IPropertyDescriptor[] descriptors; + + public LiveBeanPropertySource(AbstractLiveBeansModelElement model) { + this.model = model; + } + + public Object getEditableValue() { + // TODO Auto-generated method stub + return null; + } + + public IPropertyDescriptor[] getPropertyDescriptors() { + if (descriptors == null) { + List descrList = new ArrayList(); + Map attributes = model.getAttributes(); + Set keys = attributes.keySet(); + for (String key : keys) { + PropertyDescriptor desc = new PropertyDescriptor(key, key); + descrList.add(desc); + } + descriptors = descrList.toArray(new IPropertyDescriptor[descrList.size()]); + } + return descriptors; + } + + public Object getPropertyValue(Object id) { + return model.getAttributes().get(id); + } + + public boolean isPropertySet(Object id) { + // TODO Auto-generated method stub + return false; + } + + public void resetPropertyValue(Object id) { + // TODO Auto-generated method stub + + } + + public void setPropertyValue(Object id, Object value) { + // TODO Auto-generated method stub + + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeanRelation.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeanRelation.java new file mode 100644 index 000000000..63d073fb4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeanRelation.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2012, 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.beans.ui.live.model; + +import java.util.Map; + +/** + * A wrapper around a {@link LiveBean} used by the + * {@link LiveBeansTreeContentProvider} for displaying bean relationships + * without nesting. + * + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class LiveBeanRelation extends AbstractLiveBeansModelElement { + + private final LiveBean bean; + + private final boolean isDependency; + + public LiveBeanRelation(LiveBean bean) { + this(bean, false); + } + + public LiveBeanRelation(LiveBean bean, boolean isDependency) { + this.bean = bean; + this.isDependency = isDependency; + } + + @Override + public void addAttribute(String key, String value) { + // no-op + } + + @Override + public Map getAttributes() { + return bean.getAttributes(); + } + + public String getDisplayName() { + return bean.getDisplayName(); + } + + public boolean isDependency() { + return isDependency; + } + + public boolean isInnerBean() { + return bean.isInnerBean(); + } + + public LiveBean getBean() { + return this.bean; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeanType.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeanType.java new file mode 100644 index 000000000..a630c0a16 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeanType.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2017, 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.beans.ui.live.model; + +/** + * Wrapper to mark bean type in the UI + * + * @author Alex Boyko + * + */ +public class LiveBeanType implements DisplayName { + + private LiveBean bean; + + public LiveBeanType(LiveBean bean) { + this.bean = bean; + } + + public String getDisplayName() { + String type = bean.getBeanType(); + int idx = type.indexOf("$$"); + if (idx >= 0) { + return type.substring(0, idx); + } + return type; + } + + @Override + public String toString() { + return "LiveBeanType("+bean+")"; + } + + public LiveBean getBean() { + return bean; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansContext.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansContext.java new file mode 100644 index 000000000..8d833ab57 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansContext.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class LiveBeansContext extends LiveBeansGroup { + + public static final String ATTR_CONTEXT_ID = "contextId"; + + public static final String ATTR_CONTEXT = "context"; + + public static final String ATTR_PARENT = "parent"; + + public static final String ATTR_BEANS = "beans"; + + private LiveBeansContext parent; + + public LiveBeansContext(String label) { + super(label); + attributes.put(ATTR_CONTEXT, label); + } + + @Override + public String getDisplayName() { + return getLabel(); + } + + public LiveBeansContext getParent() { + return parent; + } + + public void setParent(LiveBeansContext parent) { + this.parent = parent; + attributes.put(ATTR_PARENT, parent.getLabel()); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansGroup.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansGroup.java new file mode 100644 index 000000000..8d464869b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansGroup.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class LiveBeansGroup extends AbstractLiveBeansModelElement { + + private final String label; + + private final List elements; + + public LiveBeansGroup(String label) { + this(label, new ArrayList()); + } + + public LiveBeansGroup(String label, List elements) { + this.label = label; + this.elements = elements; + } + + public void addElement(T bean) { + elements.add(bean); + } + + public List getElements() { + return elements; + } + + public String getDisplayName() { + return getLabel(); + } + + public String getLabel() { + return label; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof LiveBeansGroup) { + LiveBeansGroup other = (LiveBeansGroup) obj; + return Objects.equals(label, other.label) + && Objects.equals(attributes, other.attributes) + && Objects.equals(elements, other.elements); + } + return super.equals(obj); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansJsonParser.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansJsonParser.java new file mode 100644 index 000000000..9e85179cf --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansJsonParser.java @@ -0,0 +1,202 @@ +/******************************************************************************* + * Copyright (c) 2012, 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.beans.ui.live.model; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Parsing of Boot 1.x live beans. Differences between Boot 1.x and other Boot versions should be handled by protected API that can be overridden + * by more recent Boot version subclasses. + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class LiveBeansJsonParser { + + private final TypeLookup typeLookup; + + private final String jsonInput; + + private Map beansMap; + + private Map contextMap; + + private Map resourceMap; + + public LiveBeansJsonParser(TypeLookup typeLookup, String jsonInput) { + this.jsonInput = jsonInput; + this.typeLookup = typeLookup; + } + + private void groupByResource() { + for (LiveBean bean : beansMap.values()) { + String resource = bean.getResource(); + if (resourceMap.containsKey(resource)) { + LiveBeansResource group = resourceMap.get(resource); + group.addElement(bean); + } + else { + LiveBeansResource group = new LiveBeansResource(resource, bean.getTypeLookup()); + group.addElement(bean); + resourceMap.put(resource, group); + } + } + } + + protected JSONArray extractContextsJson(String json) throws JSONException { + // JSON structure is an array of context descriptions, each containing + // an array of beans + return new JSONArray(json); + } + + public LiveBeansModel parse() throws JSONException { + beansMap = new LinkedHashMap(); + contextMap = new LinkedHashMap(); + resourceMap = new LinkedHashMap(); + + JSONArray contextsArray = extractContextsJson(jsonInput); + + parseContexts(contextsArray); + populateContextDependencies(contextsArray); + groupByResource(); + + LiveBeansModel model = new LiveBeansModel(typeLookup); + model.addBeans(beansMap.values()); + model.addContexts(contextMap.values()); + model.addResources(resourceMap.values()); + return model; + } + + private void parseBeans(LiveBeansContext context, JSONArray beansArray) throws JSONException { + // construct LiveBeans + for (int i = 0; i < beansArray.length(); i++) { + JSONObject beanJson = beansArray.getJSONObject(i); + if (beanJson != null && beanJson.has(LiveBean.ATTR_BEAN)) { + LiveBean bean = parseBean(beanJson, context); + bean.addAttribute(LiveBeansContext.ATTR_CONTEXT, context.getLabel()); + context.addElement(bean); + beansMap.put(bean.getId(), bean); + } + } + } + + protected LiveBean parseBean(JSONObject beanJson, LiveBeansContext context) throws JSONException { + LiveBean bean = new LiveBean(typeLookup, beanJson.getString(LiveBean.ATTR_BEAN)); + if (beanJson.has(LiveBean.ATTR_SCOPE)) { + bean.addAttribute(LiveBean.ATTR_SCOPE, beanJson.getString(LiveBean.ATTR_SCOPE)); + } + if (beanJson.has(LiveBean.ATTR_TYPE)) { + bean.addAttribute(LiveBean.ATTR_TYPE, beanJson.getString(LiveBean.ATTR_TYPE)); + } + if (beanJson.has(LiveBean.ATTR_RESOURCE)) { + bean.addAttribute(LiveBean.ATTR_RESOURCE, beanJson.getString(LiveBean.ATTR_RESOURCE)); + } + if (typeLookup != null && typeLookup.getApplicationName() != null) { + bean.addAttribute(LiveBean.ATTR_APPLICATION, typeLookup.getApplicationName()); + } + return bean; + } + + /** + * IMPORTANT: "beans" structure in the context JSON DIFFERS between Boot 1.x and later Boot versions. It is important + * to ALWAYS extract beans from a context using this method (for example, when creating the live beans initially, or separately, when + * populating the dependencies for those live beans), as this method may be overridden for other boot versions + * @param contextJson + * @return + */ + protected JSONArray extractBeans(JSONObject contextJson) { + // Boot 1.x version + return contextJson.optJSONArray(LiveBeansContext.ATTR_BEANS); + } + + protected String getContextId(JSONObject contextJson) throws JSONException { + return contextJson.getString(LiveBeansContext.ATTR_CONTEXT); + } + + protected LiveBeansContext parseContext(JSONObject contextJson) throws JSONException { + LiveBeansContext context = new LiveBeansContext(getContextId(contextJson)); + JSONArray beansArray = extractBeans(contextJson); + if (beansArray != null) { + parseBeans(context, beansArray); + } + return context; + } + + private void parseContexts(JSONArray contextsArray) throws JSONException { + // construct LiveBeansContexts + for (int i = 0; i < contextsArray.length(); i++) { + JSONObject contextJson = contextsArray.optJSONObject(i); + if (contextJson != null) { + LiveBeansContext context = parseContext(contextJson); + contextMap.put(context.getLabel(), context); + } + } + } + + private void populateBeanDependencies(JSONArray beansArray) throws JSONException { + // populate LiveBean dependencies + for (int i = 0; i < beansArray.length(); i++) { + JSONObject beanJson = beansArray.optJSONObject(i); + if (beanJson != null && beanJson.has(LiveBean.ATTR_BEAN)) { + LiveBean bean = beansMap.get(beanJson.getString(LiveBean.ATTR_BEAN)); + JSONArray dependencies = beanJson.optJSONArray(LiveBean.ATTR_DEPENDENCIES); + if (dependencies != null) { + for (int j = 0; j < dependencies.length(); j++) { + String dependency = dependencies.getString(j); + LiveBean dependencyBean = beansMap.get(dependency); + if (dependencyBean != null) { + bean.addDependency(dependencyBean); + } + else { + LiveBean dependentBean = new LiveBean(typeLookup, dependency, true); + if (typeLookup != null && typeLookup.getApplicationName() != null) { + dependentBean.addAttribute(LiveBean.ATTR_APPLICATION, typeLookup.getApplicationName()); + } + bean.addDependency(dependentBean); + } + } + } + } + } + } + + private void populateContextDependencies(JSONArray contextsArray) throws JSONException { + // populate LiveBeanContext dependencies + for (int i = 0; i < contextsArray.length(); i++) { + JSONObject contextJson = contextsArray.optJSONObject(i); + if (contextJson != null) { + LiveBeansContext context = contextMap.get(getContextId(contextJson)); + if (!contextJson.isNull(LiveBeansContext.ATTR_PARENT)) { + String parent = contextJson.getString(LiveBeansContext.ATTR_PARENT); + LiveBeansContext parentContext = contextMap.get(parent); + if (parentContext != null) { + context.setParent(parentContext); + } + } + + // PT 164156323 - Extracting beans differs between Boot 1.x and Boot 2.x. + // Dependencies was not being populated for Boot 2.x because of a bug + // where, when populating the dependencies, beans were being extracted using Boot 1.x structure + // even for Boot 2.x. The way to fix this is to delegate to the method below which is + // overridden for Boot 2.x and handles the Boot 2.x case correctly + JSONArray beansArray = extractBeans(contextJson); // correct way + if (beansArray != null) { + populateBeanDependencies(beansArray); + } + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansJsonParser2.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansJsonParser2.java new file mode 100644 index 000000000..c51055012 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansJsonParser2.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Live Beans json parser suitable for Boot 2.x + * + * @author Alex Boyko + * + */ +public class LiveBeansJsonParser2 extends LiveBeansJsonParser { + + public LiveBeansJsonParser2(TypeLookup typeLookup, String jsonInput) { + super(typeLookup, jsonInput); + } + + @Override + protected JSONArray extractContextsJson(String json) throws JSONException { + JSONObject contextsObj = new JSONObject(json); + if (contextsObj.has("contexts")) { + JSONObject contextsMap = contextsObj.getJSONObject("contexts"); + String[] contextIds = JSONObject.getNames(contextsMap); + ArrayList contextsList = new ArrayList<>(contextIds.length); + for (Object contextId : contextIds) { + if (contextId instanceof String) { + JSONObject contextJson = contextsMap.getJSONObject((String) contextId); + contextJson.put(LiveBeansContext.ATTR_CONTEXT_ID, contextId); + contextsList.add(contextJson); + } + } + return new JSONArray(contextsList); + } else { + Object obj = new JSONTokener(json).nextValue(); + if (obj instanceof JSONArray) { + return (JSONArray) obj; + } else { + return new JSONArray(Arrays.asList(obj)); + } + } + } + + @Override + protected String getContextId(JSONObject contextJson) throws JSONException { + return contextJson.getString(LiveBeansContext.ATTR_CONTEXT_ID); + } + + @Override + @SuppressWarnings("unchecked") + protected JSONArray extractBeans(JSONObject contextJson) { + JSONObject obj = contextJson.optJSONObject(LiveBeansContext.ATTR_BEANS); + Iterable iterable = () -> obj.keys(); + return new JSONArray(StreamSupport.stream(iterable.spliterator(), false) + .map(k -> { + try { + JSONObject jsonBean = obj.getJSONObject(k); + jsonBean.put(LiveBean.ATTR_BEAN, k); + return jsonBean; + } catch (JSONException e) { + // Shouldn't happen since key exists + return null; + }}) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + ); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansModel.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansModel.java new file mode 100644 index 000000000..dadff2c79 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansModel.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +/** + * A model of a running Spring application to be graphed in the Live Beans Graph + * + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class LiveBeansModel implements Comparable { + + private final List beans; + + private final List contexts; + + private final List resources; + + private final TypeLookup typeLookup; + + public LiveBeansModel(TypeLookup typeLookup) { + this.beans = new ArrayList(); + this.contexts = new ArrayList(); + this.resources = new ArrayList(); + this.typeLookup = typeLookup; + } + + public void addBeans(Collection beansToAdd) { + beans.addAll(beansToAdd); + } + + public void addContexts(Collection contextsToAdd) { + contexts.addAll(contextsToAdd); + } + + public void addResources(Collection resourcesToAdd) { + resources.addAll(resourcesToAdd); + } + + public int compareTo(LiveBeansModel o) { + return getApplicationName().compareTo(o.getApplicationName()); + } + + public String getApplicationName() { + if (typeLookup != null) { + return typeLookup.getApplicationName(); + } + return ""; + } + + public List getBeans() { + return beans; + } + + public List getBeansByContext() { + return contexts; + } + + public List getBeansByResource() { + return resources; + } + + public TypeLookup getWorkspaceContext() { + return typeLookup; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof LiveBeansModel) { + LiveBeansModel other = (LiveBeansModel) obj; + // Should be enough to compare contexts only since this is close to raw JSON data + return contexts.equals(other.contexts) && Objects.equals(typeLookup, other.typeLookup); + } + return super.equals(obj); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansModelCollection.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansModelCollection.java new file mode 100644 index 000000000..6154a98b4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansModelCollection.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +import java.util.Set; +import java.util.TreeSet; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class LiveBeansModelCollection { + + private static LiveBeansModelCollection instance; + + public static LiveBeansModelCollection getInstance() { + if (instance == null) { + instance = new LiveBeansModelCollection(); + } + return instance; + } + + private final Set collection; + + private LiveBeansModelCollection() { + collection = new TreeSet(); + } + + public void addModel(LiveBeansModel model) { + if (collection.contains(model)) { + collection.remove(model); + } + collection.add(model); + } + + public Set getCollection() { + return collection; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansResource.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansResource.java new file mode 100644 index 000000000..74640443e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/LiveBeansResource.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class LiveBeansResource extends LiveBeansGroup { + + private final TypeLookup lookupSession; + + public LiveBeansResource(String label, TypeLookup lookupSession) { + super(label); + attributes.put(LiveBean.ATTR_RESOURCE, label); + this.lookupSession = lookupSession; + } + + public TypeLookup getTypeLookup() { + return this.lookupSession; + } + + @Override + public String getDisplayName() { + // compute the display name the first time it's needed + String label = getLabel(); + if (label.equalsIgnoreCase("null")) { + return "Container Generated"; + } else { + // Expecting the label to contain some form of + // "[file/path/to/resource.ext]" so we're going to parse out the + // last segment of the file path. + int indexStart = label.indexOf("["); + int indexEnd = label.lastIndexOf("]"); + if (indexStart > -1 && indexEnd > -1 && indexStart < indexEnd) { + return label.substring(indexStart + 1, indexEnd); + } else { + return label; + } + } + } + + public String getFileName() { + String label = getLabel(); + int indexStart = label.lastIndexOf("/"); + int indexEnd = label.lastIndexOf("]"); + if (indexStart > -1 && indexEnd > -1 && indexStart < indexEnd) { + return label.substring(indexStart + 1, indexEnd); + } + return null; + } + + public String getFileExtension() { + String filename = getFileName(); + if (filename != null) { + int idx = filename.lastIndexOf('.'); + if (idx >= 0 && idx < filename.length() - 1) { + return filename.substring(idx + 1); + } + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/SpringResource.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/SpringResource.java new file mode 100644 index 000000000..d7a11d4c6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/SpringResource.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 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.beans.ui.live.model; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.springsource.ide.eclipse.commons.frameworks.core.util.JavaTypeUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * Helper class, represents parsed info from a Resource, and provide method(s) + * to display it somehow. + */ +public class SpringResource { + + public static final String BEAN_DEFINITION_IN = "BeanDefinition defined in"; + public static final String URL = "URL"; + private static final String CF_CLASSPATH_PREFIX = "/home/vcap/app/"; + private static final String CLASS = ".class"; + + private String path; + private IProject project; + + private static final Pattern BRACKETS = Pattern.compile("\\[[^\\]]*\\]"); + + private static final String ID_PATTERN = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"; + private static final String REGEX_FQCN = ID_PATTERN + "(\\." + ID_PATTERN + ")*"; + + public SpringResource(String resourceDefinition, IProject project) { + this.project = project; + parse(resourceDefinition); + } + + private void parse(String resourceDefinition) { + Matcher matcher = BRACKETS.matcher(resourceDefinition); + if (matcher.find()) { + String type = resourceDefinition.substring(0, matcher.start()).trim(); + path = resourceDefinition.substring(matcher.start() + 1, matcher.end() - 1); + if (type.equals("file") && path.startsWith(CF_CLASSPATH_PREFIX)) { + path = path.substring(CF_CLASSPATH_PREFIX.length()); + } + } else if (Pattern.matches(REGEX_FQCN, resourceDefinition)) { + // Resource is fully qualified Java type name + path = resourceDefinition.replace('.', '/') + CLASS; + } else { + path = parseFromBeanDefIn(resourceDefinition); + if (path == null) { + path = resourceDefinition; + } + } + } + + private String parseFromBeanDefIn(String resourceDefinition) { + int beanDefInIndex = resourceDefinition.indexOf(BEAN_DEFINITION_IN); + if (beanDefInIndex >= 0 && beanDefInIndex + BEAN_DEFINITION_IN.length() + 1 < resourceDefinition.length()) { + String defInVal = resourceDefinition.substring(beanDefInIndex + BEAN_DEFINITION_IN.length() + 1); + if (Pattern.matches(REGEX_FQCN, defInVal)) { + return defInVal.replace('.', '/') + CLASS; + } + } + return null; + } + + public String getResourcePath() { + return path; + } + + public String getClassName() { + if (path != null && path.endsWith(".class")) { + String clssName = path; + + // Check if the class name is relative to either WEB-INF or + // project path (e.g. the bean is defined in the project) + int index = clssName.lastIndexOf("/WEB-INF/classes/"); + int length = "/WEB-INF/classes/".length(); + if (index >= 0) { + clssName = clssName.substring(index + length); + } else if (project != null) { + try { + String possibleType = typeFromProjectRelativePath(project, clssName); + if (possibleType != null) { + clssName = possibleType; + } + } catch (Exception e) { + Log.log(e); + } + } + + clssName = clssName.substring(0, clssName.lastIndexOf(".class")); + clssName = clssName.replaceAll("\\\\|\\/", "."); // Tolerate both '/' and '\'. + clssName = clssName.replace('$', '.'); // Replace inner classes '$' with JDT's '.' + + return clssName; + } + return null; + } + + protected String typeFromProjectRelativePath(IProject project, String fullPath) throws Exception { + IJavaProject javaProject = getJavaProject(project); + return javaProject != null ? JavaTypeUtil.getFQTypeName(javaProject, fullPath) : null; + } + + protected IJavaProject getJavaProject(IProject project) throws Exception { + if (project != null && project.isAccessible() && project.hasNature(JavaCore.NATURE_ID)) { + return JavaCore.create(project); + } + return null; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/TypeLookup.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/TypeLookup.java new file mode 100644 index 000000000..e4c056f6f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/TypeLookup.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.core.IType; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public interface TypeLookup { + + public String getApplicationName(); + + public IProject getProject(); + + IType findType(String fqName); + + IProject[] relatedProjects(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/TypeLookupImpl.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/TypeLookupImpl.java new file mode 100644 index 000000000..a16017d35 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/model/TypeLookupImpl.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.model; + +import java.util.Objects; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Assert; +import org.eclipse.jdt.core.IType; +import org.springframework.ide.eclipse.beans.ui.live.utils.JdtUtils; + +/** + * Type Lookup implementation + * + * @author Alex Boyko + * + */ +public class TypeLookupImpl implements TypeLookup { + + private final String appName; + + private final IProject project; + + public TypeLookupImpl(String appName, IProject project) { + this.appName = appName; + this.project = project; + } + + public String getApplicationName() { + return appName; + } + + public IProject getProject() { + return project; + } + + protected String cleanClassName(String className) { + String cleanClassName = className; + if (className != null) { + int ix = className.indexOf('$'); + if (ix > 0) { + cleanClassName = className.substring(0, ix); + } + else { + ix = className.indexOf('#'); + if (ix > 0) { + cleanClassName = className.substring(0, ix); + } + } + } + return cleanClassName; + } + + @Override + public IType findType(String fqName) { + IProject[] projects = relatedProjects(); + for (IProject project : projects) { + IType type = JdtUtils.getJavaType(project, cleanClassName(fqName)); + if (type != null) { + return type; + } + } + return null; + } + + @Override + public IProject[] relatedProjects() { + if (project!=null) { + return new IProject[] { project }; + } else { + return ResourcesPlugin.getWorkspace().getRoot().getProjects(); + } + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TypeLookupImpl) { + TypeLookupImpl other = (TypeLookupImpl) obj; + return Objects.equals(appName, other.appName) && Objects.equals(project, other.project); + } + return super.equals(obj); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/AbstractLiveBeansTreeContentProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/AbstractLiveBeansTreeContentProvider.java new file mode 100644 index 000000000..5f4037084 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/AbstractLiveBeansTreeContentProvider.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.tree; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBean; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeanRelation; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeanType; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansContext; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansGroup; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansResource; + +/** + * A content provider for the Live Beans tree display + * + * @author Leo Dos Santos + * @author Alex Boyko + */ +public abstract class AbstractLiveBeansTreeContentProvider implements ITreeContentProvider { + + @SuppressWarnings("unchecked") + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof LiveBeansGroup) { + LiveBeansGroup group = (LiveBeansGroup) parentElement; + if (group instanceof LiveBeansContext || group instanceof LiveBeansResource) { + List sortedBeans = new ArrayList<>(((LiveBeansGroup)group).getElements()); + sortedBeans.sort((a, b) -> a.getDisplayName().compareToIgnoreCase(b.getDisplayName())); + return sortedBeans.toArray(new LiveBean[sortedBeans.size()]); + } else { + return group.getElements().toArray(); + } + } + else if (parentElement instanceof LiveBean) { + LiveBean bean = (LiveBean) parentElement; + return getBeanChildren(bean).toArray(); + } + return null; + } + + protected List getBeanChildren(LiveBean bean) { + List children = new ArrayList<>(); + + children.add(new LiveBeanType(bean)); + + children.add(new LiveBeansGroup<>("Dependencies", bean.getDependencies() + .stream() + .map(b -> new LiveBeanRelation(b, true)) + .collect(Collectors.toList()))); + + children.add(new LiveBeansGroup<>("Injected Into", bean.getInjectedInto() + .stream() + .map(b -> new LiveBeanRelation(b)) + .collect(Collectors.toList()))); + + return children; + } + + public Object getParent(Object element) { + return null; + } + + public boolean hasChildren(Object element) { + if (element instanceof LiveBeansGroup) { + return !((LiveBeansGroup)element).getElements().isEmpty(); + } else if (element instanceof LiveBean) { + return true; + } + return false; + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/ContextGroupedBeansContentProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/ContextGroupedBeansContentProvider.java new file mode 100644 index 000000000..4ad328257 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/ContextGroupedBeansContentProvider.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2012, 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.beans.ui.live.tree; + +import java.util.List; + +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBean; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansModel; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansResource; + +/** + * Beans grouped by contexts + * + * @author Alex Boyko + * + */ +public final class ContextGroupedBeansContentProvider extends AbstractLiveBeansTreeContentProvider { + + public static final ContextGroupedBeansContentProvider INSTANCE = new ContextGroupedBeansContentProvider(); + + private ContextGroupedBeansContentProvider() {} + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof LiveBeansModel) { + LiveBeansModel model = (LiveBeansModel) inputElement; + return model.getBeansByContext().toArray(); + } + return new Object[0]; + } + + @Override + protected List getBeanChildren(LiveBean bean) { + List children = super.getBeanChildren(bean); + String resource = bean.getResource(); + if (resource != null && !resource.isEmpty()) { + // Do not add the bean as a element to the resource, otherwise + // it will appear as a child and the tree will have an infinite + // deep livebean. However, we still need the type lookup in the bean + // to resolve the types in the resource + children.add(1, new LiveBeansResource(resource, bean.getTypeLookup())); + } + return children; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/InnerBeansViewerFilter.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/InnerBeansViewerFilter.java new file mode 100644 index 000000000..bc116b458 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/InnerBeansViewerFilter.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.tree; + +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerFilter; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBean; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeanRelation; + +/** + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class InnerBeansViewerFilter extends ViewerFilter { + + @Override + public boolean select(Viewer viewer, Object parentElement, Object element) { + if (element instanceof LiveBean) { + LiveBean bean = (LiveBean) element; + return !bean.isInnerBean(); + } + else if (element instanceof LiveBeanRelation) { + LiveBeanRelation bean = (LiveBeanRelation) element; + return !bean.isInnerBean(); + } + return true; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/LiveBeansTreeLabelProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/LiveBeansTreeLabelProvider.java new file mode 100644 index 000000000..d6286a7af --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/LiveBeansTreeLabelProvider.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.tree; + +import org.eclipse.jdt.ui.ISharedImages; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.beans.ui.live.LiveBeansUiPlugin; +import org.springframework.ide.eclipse.beans.ui.live.model.DisplayName; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBean; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeanRelation; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeanType; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansGroup; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansResource; +import org.springsource.ide.eclipse.commons.livexp.ui.util.TreeElementWrappingContentProvider; + +/** + * A label provider for the Live Beans tree display + * + * @author Leo Dos Santos + * @author Alex Boyko + */ +public class LiveBeansTreeLabelProvider extends LabelProvider { + + public static final LiveBeansTreeLabelProvider INSTANCE = new LiveBeansTreeLabelProvider(); + + // public void decorate(Object element, IDecoration decoration) { + // if (element instanceof LiveBean) { + // LiveBean bean = (LiveBean) element; + // String type = bean.getBeanType(); + // String scope = bean.getScope(); + // String text = "[type=\"" + type + "\"; scope=\"" + scope + "\"]"; + // decoration.addSuffix(text); + // decoration.setForegroundColor(Display.getDefault().getSystemColor(SWT.COLOR_TITLE_INACTIVE_FOREGROUND)); + // } + // } + + @Override + public Image getImage(Object element) { + if (element instanceof TreeElementWrappingContentProvider.TreeNode) { + return getImage(((TreeElementWrappingContentProvider.TreeNode)element).getWrappedValue()); + } else if (element instanceof LiveBean) { + return LiveBeansUiPlugin.getDefault().getImageRegistry().get(LiveBeansUiPlugin.IMG_OBJS_BEAN); + } else if (element instanceof LiveBeansResource) { + String fileExtension = ((LiveBeansResource)element).getFileExtension(); + if ("class".equalsIgnoreCase(fileExtension)) { + return JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CFILE); + } else { + return LiveBeansUiPlugin.getDefault().getImageRegistry().get(LiveBeansUiPlugin.IMG_OBJS_CONFIG); + } + } else if (element instanceof LiveBeansGroup) { + return LiveBeansUiPlugin.getDefault().getImageRegistry().get(LiveBeansUiPlugin.IMG_OBJS_COLLECTION); + } else if (element instanceof LiveBeanType) { + return JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CLASS); + } else if (element instanceof LiveBeanRelation) { + // TODO: incoming/outgoing arrow images??? + return LiveBeansUiPlugin.getDefault().getImageRegistry().get(LiveBeansUiPlugin.IMG_OBJS_BEAN_REF); + } + return super.getImage(element); + } + + @Override + public String getText(Object element) { + if (element instanceof TreeElementWrappingContentProvider.TreeNode) { + return getText(((TreeElementWrappingContentProvider.TreeNode) element).getWrappedValue()); + } else if (element instanceof DisplayName) { + return ((DisplayName) element).getDisplayName(); + } + return super.getText(element); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/ResourceGroupedBeansContentProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/ResourceGroupedBeansContentProvider.java new file mode 100644 index 000000000..2699240dd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/tree/ResourceGroupedBeansContentProvider.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2012, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.tree; + +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansModel; + +/** + * Content provider for beans grouped by resources + * + * @author Alex Boyko + * + */ +public final class ResourceGroupedBeansContentProvider extends AbstractLiveBeansTreeContentProvider { + + public static final ResourceGroupedBeansContentProvider INSTANCE = new ResourceGroupedBeansContentProvider(); + + private ResourceGroupedBeansContentProvider() {} + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof LiveBeansModel) { + LiveBeansModel model = (LiveBeansModel) inputElement; + return model.getBeansByResource().toArray(); + } + return new Object[0]; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/utils/JdtUtils.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/utils/JdtUtils.java new file mode 100644 index 000000000..eb9182b35 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/utils/JdtUtils.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.beans.ui.live.utils; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; +import org.springframework.ide.eclipse.beans.ui.live.LiveBeansUiPlugin; + +/** + * JDT Utilities (subset taken from Spring-IDE core to minimize dependencies) + * + * @author Alex Boyko + * + */ +public class JdtUtils { + + /** + * Returns the corresponding Java project or null a for given + * project. + * + * @param project + * the project the Java project is requested for + * @return the requested Java project or null if the Java project + * is not defined or the project is not accessible + */ + public static IJavaProject getJavaProject(IProject project) { + if (project.isAccessible()) { + try { + if (project.hasNature(JavaCore.NATURE_ID)) { + return (IJavaProject) project.getNature(JavaCore.NATURE_ID); + } + } catch (CoreException e) { + LiveBeansUiPlugin.getDefault().getLog().log(new Status(IStatus.ERROR, LiveBeansUiPlugin.PLUGIN_ID, + "Error getting Java project for project '" + project.getName() + "'", e)); + } + } + return null; + } + + /** + * Returns the corresponding Java type for given full-qualified class name. + * + * @param project + * the JDT project the class belongs to + * @param className + * the full qualified class name of the requested Java type + * @return the requested Java type or null if the class is not defined or the + * project is not accessible + */ + public static IType getJavaType(IProject project, String className) { + IJavaProject javaProject = JdtUtils.getJavaProject(project); + if (className != null) { + // For inner classes replace '$' by '.' + int pos = className.lastIndexOf('$'); + if (pos > 0) { + className = className.replace('$', '.'); + } + try { + IType type = null; + // First look for the type in the Java project + if (javaProject != null) { + // TODO CD not sure why we need + type = javaProject.findType(className, new NullProgressMonitor()); + // type = javaProject.findType(className); + if (type != null) { + return type; + } + } + + // Then look for the type in the referenced Java projects + for (IProject refProject : project.getReferencedProjects()) { + IJavaProject refJavaProject = JdtUtils.getJavaProject(refProject); + if (refJavaProject != null) { + type = refJavaProject.findType(className); + if (type != null) { + return type; + } + } + } + + // fall back and try to locate the class using AJDT + // TODO: uncomment this call + // return getAjdtType(project, className); + } catch (CoreException e) { + LiveBeansUiPlugin.getDefault().getLog().log(new Status(IStatus.ERROR, LiveBeansUiPlugin.PLUGIN_ID, + "Error getting Java type '" + className + "'", e)); + } + } + + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/utils/LiveBeanUtil.java b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/utils/LiveBeanUtil.java new file mode 100644 index 000000000..e01f8b466 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.beans.ui.live/src/org/springframework/ide/eclipse/beans/ui/live/utils/LiveBeanUtil.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 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.beans.ui.live.utils; + +import org.eclipse.jdt.core.IType; +import org.springframework.ide.eclipse.beans.ui.live.model.AbstractLiveBeansModelElement; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBean; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeanRelation; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansResource; +import org.springframework.ide.eclipse.beans.ui.live.model.SpringResource; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; +import org.springsource.ide.eclipse.commons.ui.SpringUIUtils; + +public class LiveBeanUtil { + + /** + * Navigates to the type IN the resource definition of the bean, as opposed to actual bean + * type. + *

+ * This method specifically looks at the resource attribute in the live bean to obtain a type. + *

+ * This is in contrast to {@link #navigateToType(LiveBean)}, where navigation + * occurs to the actual type of the bean. + *

+ * Type of the Bean and the type in the Resource Definition of the Bean may NOT + * necessarily be the same. + * + * @param bean + */ + public static void navigateToResource(AbstractLiveBeansModelElement element) { + LiveBean liveBean = null; + + if (element instanceof LiveBean) { + liveBean = (LiveBean) element; + } else if (element instanceof LiveBeanRelation) { + liveBean = ((LiveBeanRelation) element).getBean(); + } + + // Parse the type from the resource in the live bean and then navigate to that type + if (liveBean != null) { + TypeLookup typeLookup = liveBean.getTypeLookup(); + String resource = liveBean.getResource(); + String parsedType = parseType(resource, typeLookup); + openInEditor(typeLookup, parsedType); + } else if (element instanceof LiveBeansResource) { + LiveBeansResource resource = (LiveBeansResource) element; + TypeLookup typeLookup = resource.getTypeLookup(); + String resourceVal = null; + if (resource.getAttributes() != null) { + resourceVal = resource.getAttributes().get(LiveBean.ATTR_RESOURCE); + } + if (resourceVal == null) { + resourceVal = resource.getLabel(); + } + if (typeLookup != null && resourceVal != null) { + String parsedType = parseType(resourceVal, typeLookup); + openInEditor(typeLookup, parsedType); + } + } + } + + /** + * Navigates to the bean TYPE in the live bean. It does NOT look at the resource + * definition, UNLESS the type happens to be a proxy. In this case, it will + * navigate to the resource definition. + * + * @param bean + */ + public static void navigateToType(LiveBean bean) { + TypeLookup appName = bean.getTypeLookup(); + String beanClass = bean.getBeanType(); + if (appName != null) { + // Remove $$EnhancerBySpringCGLIB$$ + beanClass = stripCGLib(beanClass); + if (beanClass != null && beanClass.trim().length() > 0) { + if (beanClass.startsWith("com.sun.proxy")) { + // Special case for proxy beans, extract the type + // from the resource field + navigateToResource(bean); + } else { + openInEditor(appName, beanClass.replace('$', '.')); + } + } else { + // No type field, so infer class from bean ID + openInEditor(appName, bean.getId()); + } + } + } + + private static String stripCGLib(String beanClass) { + if (beanClass != null) { + int chop = beanClass.indexOf("$$EnhancerBySpringCGLIB$$"); + if (chop >= 0) { + beanClass = beanClass.substring(0, chop); + } + + chop = beanClass.indexOf("$$Lambda$"); + if (chop >= 0) { + beanClass = beanClass.substring(0, chop); + } + } + + return beanClass; + } + + private static String parseType(String resource, TypeLookup typeLookup) { + if (resource != null && resource.trim().length() > 0 && !resource.equalsIgnoreCase("null")) { + SpringResource springResource = new SpringResource(resource, typeLookup.getProject()); + return springResource.getClassName(); + } + return null; + } + + private static void openInEditor(TypeLookup workspaceContext, String className) { + if (className != null) { + IType type = workspaceContext.findType(className); + if (type != null) { + SpringUIUtils.openInEditor(type); + } + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.classpath new file mode 100644 index 000000000..c476024b7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.classpath @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.project new file mode 100644 index 000000000..fe9882a67 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.project @@ -0,0 +1,28 @@ + + + org.springframework.ide.eclipse.boot.dash.azure + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..9f6ece88b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.settings/org.eclipse.jdt.ui.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..c743e1c07 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,59 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=false +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_type_arguments=false +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=false +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.settings/org.springframework.ide.eclipse.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.settings/org.springframework.ide.eclipse.prefs new file mode 100644 index 000000000..a12794d68 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/.settings/org.springframework.ide.eclipse.prefs @@ -0,0 +1,2 @@ +boot.validation.initialized=true +eclipse.preferences.version=1 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/META-INF/MANIFEST.MF b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/META-INF/MANIFEST.MF new file mode 100755 index 000000000..87df26a02 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/META-INF/MANIFEST.MF @@ -0,0 +1,132 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-SymbolicName: org.springframework.ide.eclipse.boot.dash.azure;singleton:=true +Bundle-Version: 4.8.1.qualifier +Bundle-Vendor: Pivotal Inc +Bundle-Name: Spring Boot Dash CF Support +Require-Bundle: org.eclipse.ui, + org.eclipse.ui.ide, + org.eclipse.core.runtime, + org.springframework.ide.eclipse.boot.launch, + org.springframework.ide.eclipse.boot, + org.springframework.ide.eclipse.boot.dash, + org.springsource.ide.eclipse.commons.livexp, + org.springsource.ide.eclipse.commons.frameworks.core, + org.eclipse.debug.core, + org.eclipse.jdt.core, + com.google.guava, + com.google.gson, + org.springframework.ide.eclipse.beans.ui.live +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Activator: org.springframework.ide.eclipse.boot.dash.azure.BootDashAzurePlugin +Bundle-ActivationPolicy: lazy +Export-Package: org.springframework.ide.eclipse.boot.dash.azure +Automatic-Module-Name: org.springframework.ide.eclipse.boot.dash.azure +Bundle-ClassPath: lib/accessors-smart-1.2.jar, + lib/activation-1.1.jar, + lib/adal4j-1.6.4.jar, + lib/adapter-rxjava-2.6.2.jar, + lib/animal-sniffer-annotations-1.18.jar, + lib/applicationinsights-core-2.5.1.jar, + lib/asm-5.0.4.jar, + lib/azure-1.30.0.jar, + lib/azure-annotations-1.7.0.jar, + lib/azure-arm-client-runtime-1.6.5.jar, + lib/azure-auth-helper-0.2.0.jar, + lib/azure-client-authentication-1.6.15.jar, + lib/azure-client-runtime-1.7.0.jar, + lib/azure-keyvault-1.0.0.jar, + lib/azure-keyvault-core-0.8.0.jar, + lib/azure-keyvault-webkey-1.0.0.jar, + lib/azure-maven-plugin-common-0.1.0.jar, + lib/azure-mgmt-appplatform-1.0.0-beta-2.jar, + lib/azure-mgmt-appservice-1.30.0.jar, + lib/azure-mgmt-batch-1.30.0.jar, + lib/azure-mgmt-batchai-1.30.0.jar, + lib/azure-mgmt-cdn-1.30.0.jar, + lib/azure-mgmt-compute-1.30.0.jar, + lib/azure-mgmt-containerinstance-1.30.0.jar, + lib/azure-mgmt-containerregistry-1.30.0.jar, + lib/azure-mgmt-containerservice-1.30.0.jar, + lib/azure-mgmt-cosmosdb-1.30.0.jar, + lib/azure-mgmt-dns-1.30.0.jar, + lib/azure-mgmt-eventhub-1.30.0.jar, + lib/azure-mgmt-graph-rbac-1.30.0.jar, + lib/azure-mgmt-keyvault-1.30.0.jar, + lib/azure-mgmt-locks-1.30.0.jar, + lib/azure-mgmt-monitor-1.30.0.jar, + lib/azure-mgmt-msi-1.30.0.jar, + lib/azure-mgmt-network-1.30.0.jar, + lib/azure-mgmt-redis-1.30.0.jar, + lib/azure-mgmt-resources-1.30.0.jar, + lib/azure-mgmt-search-1.30.0.jar, + lib/azure-mgmt-servicebus-1.30.0.jar, + lib/azure-mgmt-sql-1.30.0.jar, + lib/azure-mgmt-storage-1.30.0.jar, + lib/azure-mgmt-trafficmanager-1.30.0.jar, + lib/azure-storage-6.1.0.jar, + lib/btf-1.2.jar, + lib/checker-qual-2.8.1.jar, + lib/client-runtime-1.7.0.jar, + lib/commons-codec-1.11.jar, + lib/commons-collections4-4.4.jar, + lib/commons-io-2.6.jar, + lib/commons-lang3-3.9.jar, + lib/commons-logging-1.2.jar, + lib/converter-jackson-2.5.0.jar, + lib/error_prone_annotations-2.3.2.jar, + lib/failureaccess-1.0.1.jar, + lib/FastInfoset-1.2.16.jar, + lib/hamcrest-core-1.3.jar, + lib/httpclient-4.5.9.jar, + lib/httpcore-4.4.11.jar, + lib/istack-commons-runtime-3.0.8.jar, + lib/j2objc-annotations-1.3.jar, + lib/jackson-annotations-2.9.0.jar, + lib/jackson-core-2.9.4.jar, + lib/jackson-coreutils-1.9.jar, + lib/jackson-databind-2.9.6.jar, + lib/jackson-datatype-joda-2.10.0.jar, + lib/jakarta.activation-api-1.2.1.jar, + lib/jakarta.xml.bind-api-2.3.2.jar, + lib/jansi-1.17.1.jar, + lib/javax.mail-1.6.1.jar, + lib/javax.servlet-api-3.1.0.jar, + lib/jaxb-api-2.3.0.jar, + lib/jaxb-runtime-2.3.2.jar, + lib/jcip-annotations-1.0-1.jar, + lib/jetty-http-9.4.20.v20190813.jar, + lib/jetty-io-9.4.20.v20190813.jar, + lib/jetty-security-9.4.20.v20190813.jar, + lib/jetty-server-9.4.20.v20190813.jar, + lib/jetty-servlet-9.4.20.v20190813.jar, + lib/jetty-util-9.4.20.v20190813.jar, + lib/jjwt-0.9.1.jar, + lib/joda-time-2.1.jar, + lib/jopt-simple-5.0.3.jar, + lib/json-schema-core-1.2.10.jar, + lib/json-schema-validator-2.2.10.jar, + lib/json-smart-2.3.jar, + lib/jsr305-3.0.1.jar, + lib/junit-4.12.jar, + lib/lang-tag-1.4.4.jar, + lib/libphonenumber-8.0.0.jar, + lib/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar, + lib/logging-interceptor-3.12.2.jar, + lib/mailapi-1.4.3.jar, + lib/msg-simple-1.1.jar, + lib/nimbus-jose-jwt-8.4.jar, + lib/nimbus-jose-jwt-8.6.jar, + lib/oauth2-oidc-sdk-6.5.jar, + lib/okhttp-3.12.6.jar, + lib/okhttp-urlconnection-3.12.2.jar, + lib/okio-1.15.0.jar, + lib/org.jacoco.agent-0.8.4-runtime.jar, + lib/retrofit-2.5.0.jar, + lib/rhino-1.7.7.1.jar, + lib/rxjava-1.3.8.jar, + lib/slf4j-api-1.7.21.jar, + lib/stax-ex-1.8.1.jar, + lib/txw2-2.3.2.jar, + lib/uri-template-0.9.jar, + . diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/TODO.txt b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/TODO.txt new file mode 100644 index 000000000..2f8194949 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/TODO.txt @@ -0,0 +1,3 @@ +- Add connect/disconnect action (probably this should be done by refactoring the existing action for CF to make it + 'generic'. +- Persist connected state and try to reconnect automatically \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/build.properties b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/build.properties new file mode 100755 index 000000000..1431c42b3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/build.properties @@ -0,0 +1,113 @@ +output.. = bin/ +source.. = src/ +bin.includes = META-INF/,\ + plugin.xml,\ + icons/,\ + .,\ + lib/accessors-smart-1.2.jar,\ + lib/activation-1.1.jar,\ + lib/adal4j-1.6.4.jar,\ + lib/adapter-rxjava-2.6.2.jar,\ + lib/animal-sniffer-annotations-1.18.jar,\ + lib/applicationinsights-core-2.5.1.jar,\ + lib/asm-5.0.4.jar,\ + lib/azure-1.30.0.jar,\ + lib/azure-annotations-1.7.0.jar,\ + lib/azure-arm-client-runtime-1.6.5.jar,\ + lib/azure-auth-helper-0.2.0.jar,\ + lib/azure-client-authentication-1.6.15.jar,\ + lib/azure-client-runtime-1.7.0.jar,\ + lib/azure-keyvault-1.0.0.jar,\ + lib/azure-keyvault-core-0.8.0.jar,\ + lib/azure-keyvault-webkey-1.0.0.jar,\ + lib/azure-maven-plugin-common-0.1.0.jar,\ + lib/azure-mgmt-appplatform-1.0.0-beta-2.jar,\ + lib/azure-mgmt-appservice-1.30.0.jar,\ + lib/azure-mgmt-batch-1.30.0.jar,\ + lib/azure-mgmt-batchai-1.30.0.jar,\ + lib/azure-mgmt-cdn-1.30.0.jar,\ + lib/azure-mgmt-compute-1.30.0.jar,\ + lib/azure-mgmt-containerinstance-1.30.0.jar,\ + lib/azure-mgmt-containerregistry-1.30.0.jar,\ + lib/azure-mgmt-containerservice-1.30.0.jar,\ + lib/azure-mgmt-cosmosdb-1.30.0.jar,\ + lib/azure-mgmt-dns-1.30.0.jar,\ + lib/azure-mgmt-eventhub-1.30.0.jar,\ + lib/azure-mgmt-graph-rbac-1.30.0.jar,\ + lib/azure-mgmt-keyvault-1.30.0.jar,\ + lib/azure-mgmt-locks-1.30.0.jar,\ + lib/azure-mgmt-monitor-1.30.0.jar,\ + lib/azure-mgmt-msi-1.30.0.jar,\ + lib/azure-mgmt-network-1.30.0.jar,\ + lib/azure-mgmt-redis-1.30.0.jar,\ + lib/azure-mgmt-resources-1.30.0.jar,\ + lib/azure-mgmt-search-1.30.0.jar,\ + lib/azure-mgmt-servicebus-1.30.0.jar,\ + lib/azure-mgmt-sql-1.30.0.jar,\ + lib/azure-mgmt-storage-1.30.0.jar,\ + lib/azure-mgmt-trafficmanager-1.30.0.jar,\ + lib/azure-storage-6.1.0.jar,\ + lib/btf-1.2.jar,\ + lib/checker-qual-2.8.1.jar,\ + lib/client-runtime-1.7.0.jar,\ + lib/commons-codec-1.11.jar,\ + lib/commons-collections4-4.4.jar,\ + lib/commons-io-2.6.jar,\ + lib/commons-lang3-3.9.jar,\ + lib/commons-logging-1.2.jar,\ + lib/converter-jackson-2.5.0.jar,\ + lib/error_prone_annotations-2.3.2.jar,\ + lib/failureaccess-1.0.1.jar,\ + lib/FastInfoset-1.2.16.jar,\ + lib/hamcrest-core-1.3.jar,\ + lib/httpclient-4.5.9.jar,\ + lib/httpcore-4.4.11.jar,\ + lib/istack-commons-runtime-3.0.8.jar,\ + lib/j2objc-annotations-1.3.jar,\ + lib/jackson-annotations-2.9.0.jar,\ + lib/jackson-core-2.9.4.jar,\ + lib/jackson-coreutils-1.9.jar,\ + lib/jackson-databind-2.9.6.jar,\ + lib/jackson-datatype-joda-2.10.0.jar,\ + lib/jakarta.activation-api-1.2.1.jar,\ + lib/jakarta.xml.bind-api-2.3.2.jar,\ + lib/jansi-1.17.1.jar,\ + lib/javax.mail-1.6.1.jar,\ + lib/javax.servlet-api-3.1.0.jar,\ + lib/jaxb-api-2.3.0.jar,\ + lib/jaxb-runtime-2.3.2.jar,\ + lib/jcip-annotations-1.0-1.jar,\ + lib/jetty-http-9.4.20.v20190813.jar,\ + lib/jetty-io-9.4.20.v20190813.jar,\ + lib/jetty-security-9.4.20.v20190813.jar,\ + lib/jetty-server-9.4.20.v20190813.jar,\ + lib/jetty-servlet-9.4.20.v20190813.jar,\ + lib/jetty-util-9.4.20.v20190813.jar,\ + lib/jjwt-0.9.1.jar,\ + lib/joda-time-2.1.jar,\ + lib/jopt-simple-5.0.3.jar,\ + lib/json-schema-core-1.2.10.jar,\ + lib/json-schema-validator-2.2.10.jar,\ + lib/json-smart-2.3.jar,\ + lib/jsr305-3.0.1.jar,\ + lib/junit-4.12.jar,\ + lib/lang-tag-1.4.4.jar,\ + lib/libphonenumber-8.0.0.jar,\ + lib/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar,\ + lib/logging-interceptor-3.12.2.jar,\ + lib/mailapi-1.4.3.jar,\ + lib/msg-simple-1.1.jar,\ + lib/nimbus-jose-jwt-8.4.jar,\ + lib/nimbus-jose-jwt-8.6.jar,\ + lib/oauth2-oidc-sdk-6.5.jar,\ + lib/okhttp-3.12.6.jar,\ + lib/okhttp-urlconnection-3.12.2.jar,\ + lib/okio-1.15.0.jar,\ + lib/org.jacoco.agent-0.8.4-runtime.jar,\ + lib/retrofit-2.5.0.jar,\ + lib/rhino-1.7.7.1.jar,\ + lib/rxjava-1.3.8.jar,\ + lib/slf4j-api-1.7.21.jar,\ + lib/stax-ex-1.8.1.jar,\ + lib/txw2-2.3.2.jar,\ + lib/uri-template-0.9.jar diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/create-new-target-steps.md b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/create-new-target-steps.md new file mode 100644 index 000000000..14a86f9a2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/create-new-target-steps.md @@ -0,0 +1,27 @@ +Creating new 'Run Target Type' of boot dashboard. +--------------------------------------------------- + +These are the very roughly sketched steps taken to create the 'Azure Spring Cloud' target. + +- Create a eclipse plugin to host your code +- make it depend on `org.springframework.ide.eclipse.boot.dash` +- Setup a class to define your 'injections' via the boot dash's SimpleDIContainer + - see org.springframework.ide.eclipse.boot.dash.azure.BootDashInjections + - wire it into BootDash via eclipse extension point org.springframework.ide.eclipse.boot.dash.injections +- Create a class for your new RunTargetType (AzureRunTargetType). + - make it extend AbstractRunTargetType + - implement all abstract methods with simple stubs (e.g. many can throw 'not implemented yet' exceptsion) + - Create a injection of this class in your 'BootDashInjections' class. + +At this point you can run your creation in a Eclipse runtime workbench. When you access the pulldown menu +under the '+' icon in the dash your target should show (might not do anything yet when selected if you have +not implemented it yet.) + +Implement a dialog UI that prompts the user for all the necessary infos to create a target +------------------------------------------------------------------------------------------- + + + - authenticate with the platform + - choose a target (i.e. in cf api url, user org, space), for Azure... credentials (oauth token), subscription, resource group, service + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons-src/azure-inactive.svg b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons-src/azure-inactive.svg new file mode 100644 index 000000000..4f40836bd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons-src/azure-inactive.svg @@ -0,0 +1,100 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons-src/azure.svg b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons-src/azure.svg new file mode 100644 index 000000000..a06eb596f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons-src/azure.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure-inactive.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure-inactive.png new file mode 100644 index 000000000..6b301a856 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure-inactive.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure-inactive@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure-inactive@2x.png new file mode 100644 index 000000000..c5a3c35aa Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure-inactive@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure.png new file mode 100644 index 000000000..47dfc1414 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure@2x.png new file mode 100644 index 000000000..21135a655 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/azure@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/cloud_obj.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/cloud_obj.png new file mode 100644 index 000000000..7633c35fb Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/icons/cloud_obj.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/FastInfoset-1.2.16-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/FastInfoset-1.2.16-sources.jar new file mode 100644 index 000000000..5ec201f7f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/FastInfoset-1.2.16-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/accessors-smart-1.2-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/accessors-smart-1.2-sources.jar new file mode 100644 index 000000000..e97998cbc Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/accessors-smart-1.2-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/activation-1.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/activation-1.1-sources.jar new file mode 100644 index 000000000..316df7e52 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/activation-1.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/adal4j-1.6.4-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/adal4j-1.6.4-sources.jar new file mode 100644 index 000000000..de5904aff Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/adal4j-1.6.4-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/adapter-rxjava-2.6.2-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/adapter-rxjava-2.6.2-sources.jar new file mode 100644 index 000000000..2690bc10b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/adapter-rxjava-2.6.2-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/animal-sniffer-annotations-1.18-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/animal-sniffer-annotations-1.18-sources.jar new file mode 100644 index 000000000..701a81935 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/animal-sniffer-annotations-1.18-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/applicationinsights-core-2.5.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/applicationinsights-core-2.5.1-sources.jar new file mode 100644 index 000000000..de042a7a0 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/applicationinsights-core-2.5.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/asm-5.0.4-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/asm-5.0.4-sources.jar new file mode 100644 index 000000000..c2765f990 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/asm-5.0.4-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-1.30.0-sources.jar new file mode 100644 index 000000000..4496bb8e4 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-annotations-1.7.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-annotations-1.7.0-sources.jar new file mode 100644 index 000000000..7a8900c31 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-annotations-1.7.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-arm-client-runtime-1.6.5-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-arm-client-runtime-1.6.5-sources.jar new file mode 100644 index 000000000..080f527a1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-arm-client-runtime-1.6.5-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-auth-helper-0.2.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-auth-helper-0.2.0-sources.jar new file mode 100644 index 000000000..23d4f66de Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-auth-helper-0.2.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-client-authentication-1.6.15-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-client-authentication-1.6.15-sources.jar new file mode 100644 index 000000000..8463ba798 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-client-authentication-1.6.15-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-client-runtime-1.7.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-client-runtime-1.7.0-sources.jar new file mode 100644 index 000000000..da68107c1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-client-runtime-1.7.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-keyvault-1.0.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-keyvault-1.0.0-sources.jar new file mode 100644 index 000000000..4e20b3d12 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-keyvault-1.0.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-keyvault-core-0.8.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-keyvault-core-0.8.0-sources.jar new file mode 100644 index 000000000..f2d056b4a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-keyvault-core-0.8.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-keyvault-webkey-1.0.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-keyvault-webkey-1.0.0-sources.jar new file mode 100644 index 000000000..a3acbed7b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-keyvault-webkey-1.0.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-maven-plugin-common-0.1.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-maven-plugin-common-0.1.0-sources.jar new file mode 100644 index 000000000..c6c791783 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-maven-plugin-common-0.1.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-appplatform-1.0.0-beta-2-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-appplatform-1.0.0-beta-2-sources.jar new file mode 100644 index 000000000..6851678fe Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-appplatform-1.0.0-beta-2-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-appservice-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-appservice-1.30.0-sources.jar new file mode 100644 index 000000000..57a269c0c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-appservice-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-batch-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-batch-1.30.0-sources.jar new file mode 100644 index 000000000..71d567678 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-batch-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-batchai-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-batchai-1.30.0-sources.jar new file mode 100644 index 000000000..85bf32395 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-batchai-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-cdn-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-cdn-1.30.0-sources.jar new file mode 100644 index 000000000..52a04005f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-cdn-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-compute-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-compute-1.30.0-sources.jar new file mode 100644 index 000000000..b6af0b016 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-compute-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-containerinstance-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-containerinstance-1.30.0-sources.jar new file mode 100644 index 000000000..9df658a8b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-containerinstance-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-containerregistry-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-containerregistry-1.30.0-sources.jar new file mode 100644 index 000000000..261129a0e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-containerregistry-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-containerservice-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-containerservice-1.30.0-sources.jar new file mode 100644 index 000000000..c7896be72 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-containerservice-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-cosmosdb-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-cosmosdb-1.30.0-sources.jar new file mode 100644 index 000000000..d9bcd9845 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-cosmosdb-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-dns-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-dns-1.30.0-sources.jar new file mode 100644 index 000000000..3ad6ee9e5 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-dns-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-eventhub-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-eventhub-1.30.0-sources.jar new file mode 100644 index 000000000..2f140f22c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-eventhub-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-graph-rbac-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-graph-rbac-1.30.0-sources.jar new file mode 100644 index 000000000..b425b939e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-graph-rbac-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-keyvault-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-keyvault-1.30.0-sources.jar new file mode 100644 index 000000000..033649d33 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-keyvault-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-locks-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-locks-1.30.0-sources.jar new file mode 100644 index 000000000..2f32a5cc1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-locks-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-monitor-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-monitor-1.30.0-sources.jar new file mode 100644 index 000000000..a0c0f1c10 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-monitor-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-msi-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-msi-1.30.0-sources.jar new file mode 100644 index 000000000..8feaf3a94 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-msi-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-network-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-network-1.30.0-sources.jar new file mode 100644 index 000000000..dea4daa41 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-network-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-redis-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-redis-1.30.0-sources.jar new file mode 100644 index 000000000..b458065cb Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-redis-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-resources-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-resources-1.30.0-sources.jar new file mode 100644 index 000000000..8c91c6dff Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-resources-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-search-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-search-1.30.0-sources.jar new file mode 100644 index 000000000..dcd5a63a7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-search-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-servicebus-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-servicebus-1.30.0-sources.jar new file mode 100644 index 000000000..2522187f3 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-servicebus-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-sql-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-sql-1.30.0-sources.jar new file mode 100644 index 000000000..4747090d1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-sql-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-storage-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-storage-1.30.0-sources.jar new file mode 100644 index 000000000..dfe6b5216 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-storage-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-trafficmanager-1.30.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-trafficmanager-1.30.0-sources.jar new file mode 100644 index 000000000..1e932085e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-mgmt-trafficmanager-1.30.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-storage-6.1.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-storage-6.1.0-sources.jar new file mode 100644 index 000000000..3d0ef8b33 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/azure-storage-6.1.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/btf-1.2-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/btf-1.2-sources.jar new file mode 100644 index 000000000..006c47ac8 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/btf-1.2-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/checker-qual-2.8.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/checker-qual-2.8.1-sources.jar new file mode 100644 index 000000000..e91110e22 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/checker-qual-2.8.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/client-runtime-1.7.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/client-runtime-1.7.0-sources.jar new file mode 100644 index 000000000..e66d0baa1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/client-runtime-1.7.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-codec-1.11-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-codec-1.11-sources.jar new file mode 100644 index 000000000..af205da85 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-codec-1.11-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-collections4-4.4-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-collections4-4.4-sources.jar new file mode 100644 index 000000000..cb4f2d57e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-collections4-4.4-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-io-2.6-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-io-2.6-sources.jar new file mode 100644 index 000000000..231cbe45d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-io-2.6-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-lang3-3.9-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-lang3-3.9-sources.jar new file mode 100644 index 000000000..78ea8e4cd Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-lang3-3.9-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-logging-1.2-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-logging-1.2-sources.jar new file mode 100644 index 000000000..10655e50d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/commons-logging-1.2-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/converter-jackson-2.5.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/converter-jackson-2.5.0-sources.jar new file mode 100644 index 000000000..9caea2930 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/converter-jackson-2.5.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/error_prone_annotations-2.3.2-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/error_prone_annotations-2.3.2-sources.jar new file mode 100644 index 000000000..c3e0bd5b7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/error_prone_annotations-2.3.2-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/failureaccess-1.0.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/failureaccess-1.0.1-sources.jar new file mode 100644 index 000000000..0c429f237 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/failureaccess-1.0.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/gson-2.8.5-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/gson-2.8.5-sources.jar new file mode 100644 index 000000000..a64f4e732 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/gson-2.8.5-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/guava-28.1-jre-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/guava-28.1-jre-sources.jar new file mode 100644 index 000000000..cbaee13d7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/guava-28.1-jre-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/hamcrest-core-1.3-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/hamcrest-core-1.3-sources.jar new file mode 100644 index 000000000..c3c110b4d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/hamcrest-core-1.3-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/httpclient-4.5.9-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/httpclient-4.5.9-sources.jar new file mode 100644 index 000000000..84fdc2bd5 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/httpclient-4.5.9-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/httpcore-4.4.11-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/httpcore-4.4.11-sources.jar new file mode 100644 index 000000000..53ea01b2d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/httpcore-4.4.11-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/istack-commons-runtime-3.0.8-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/istack-commons-runtime-3.0.8-sources.jar new file mode 100644 index 000000000..bdf1ef89b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/istack-commons-runtime-3.0.8-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/j2objc-annotations-1.3-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/j2objc-annotations-1.3-sources.jar new file mode 100644 index 000000000..282ab73bd Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/j2objc-annotations-1.3-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-annotations-2.9.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-annotations-2.9.0-sources.jar new file mode 100644 index 000000000..dfd62d00c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-annotations-2.9.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-core-2.9.4-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-core-2.9.4-sources.jar new file mode 100644 index 000000000..4bafa98dc Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-core-2.9.4-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-coreutils-1.9-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-coreutils-1.9-sources.jar new file mode 100644 index 000000000..b701075f9 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-coreutils-1.9-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-databind-2.9.6-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-databind-2.9.6-sources.jar new file mode 100644 index 000000000..100e8cef4 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-databind-2.9.6-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-datatype-joda-2.10.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-datatype-joda-2.10.0-sources.jar new file mode 100644 index 000000000..6e61c2dce Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jackson-datatype-joda-2.10.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jakarta.activation-api-1.2.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jakarta.activation-api-1.2.1-sources.jar new file mode 100644 index 000000000..47eccc347 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jakarta.activation-api-1.2.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jakarta.xml.bind-api-2.3.2-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jakarta.xml.bind-api-2.3.2-sources.jar new file mode 100644 index 000000000..f92b68a76 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jakarta.xml.bind-api-2.3.2-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jansi-1.17.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jansi-1.17.1-sources.jar new file mode 100644 index 000000000..d9e245cf1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jansi-1.17.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/javax.mail-1.6.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/javax.mail-1.6.1-sources.jar new file mode 100644 index 000000000..a1658be35 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/javax.mail-1.6.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/javax.servlet-api-3.1.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/javax.servlet-api-3.1.0-sources.jar new file mode 100644 index 000000000..5fde4c9dd Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/javax.servlet-api-3.1.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jaxb-api-2.3.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jaxb-api-2.3.0-sources.jar new file mode 100644 index 000000000..ea96252df Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jaxb-api-2.3.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jaxb-runtime-2.3.2-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jaxb-runtime-2.3.2-sources.jar new file mode 100644 index 000000000..8237c6ccd Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jaxb-runtime-2.3.2-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jcip-annotations-1.0-1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jcip-annotations-1.0-1-sources.jar new file mode 100644 index 000000000..5dffa51e4 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jcip-annotations-1.0-1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-http-9.4.20.v20190813-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-http-9.4.20.v20190813-sources.jar new file mode 100644 index 000000000..380ca1dbf Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-http-9.4.20.v20190813-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-io-9.4.20.v20190813-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-io-9.4.20.v20190813-sources.jar new file mode 100644 index 000000000..e490feace Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-io-9.4.20.v20190813-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-security-9.4.20.v20190813-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-security-9.4.20.v20190813-sources.jar new file mode 100644 index 000000000..7ef696c68 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-security-9.4.20.v20190813-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-server-9.4.20.v20190813-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-server-9.4.20.v20190813-sources.jar new file mode 100644 index 000000000..69cd0d8f6 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-server-9.4.20.v20190813-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-servlet-9.4.20.v20190813-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-servlet-9.4.20.v20190813-sources.jar new file mode 100644 index 000000000..8ab0945a6 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-servlet-9.4.20.v20190813-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-util-9.4.20.v20190813-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-util-9.4.20.v20190813-sources.jar new file mode 100644 index 000000000..e50420e94 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jetty-util-9.4.20.v20190813-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jjwt-0.9.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jjwt-0.9.1-sources.jar new file mode 100644 index 000000000..25a2fafb6 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jjwt-0.9.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/joda-time-2.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/joda-time-2.1-sources.jar new file mode 100644 index 000000000..44e4ed82e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/joda-time-2.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jopt-simple-5.0.3-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jopt-simple-5.0.3-sources.jar new file mode 100644 index 000000000..16f961193 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jopt-simple-5.0.3-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/json-schema-core-1.2.10-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/json-schema-core-1.2.10-sources.jar new file mode 100644 index 000000000..35bfc0dce Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/json-schema-core-1.2.10-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/json-schema-validator-2.2.10-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/json-schema-validator-2.2.10-sources.jar new file mode 100644 index 000000000..65e574f96 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/json-schema-validator-2.2.10-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/json-smart-2.3-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/json-smart-2.3-sources.jar new file mode 100644 index 000000000..38be4588a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/json-smart-2.3-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jsr305-3.0.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jsr305-3.0.1-sources.jar new file mode 100644 index 000000000..a1e97c5a0 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/jsr305-3.0.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/junit-4.12-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/junit-4.12-sources.jar new file mode 100644 index 000000000..884f92f59 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/junit-4.12-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/lang-tag-1.4.4-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/lang-tag-1.4.4-sources.jar new file mode 100644 index 000000000..10d5d880d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/lang-tag-1.4.4-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/libphonenumber-8.0.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/libphonenumber-8.0.0-sources.jar new file mode 100644 index 000000000..d5f6957fe Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/libphonenumber-8.0.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/logging-interceptor-3.12.2-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/logging-interceptor-3.12.2-sources.jar new file mode 100644 index 000000000..164b72b9c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/logging-interceptor-3.12.2-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/mailapi-1.4.3-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/mailapi-1.4.3-sources.jar new file mode 100644 index 000000000..a1346d19c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/mailapi-1.4.3-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/msg-simple-1.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/msg-simple-1.1-sources.jar new file mode 100644 index 000000000..295198215 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/msg-simple-1.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/nimbus-jose-jwt-8.4-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/nimbus-jose-jwt-8.4-sources.jar new file mode 100644 index 000000000..414857564 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/nimbus-jose-jwt-8.4-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/oauth2-oidc-sdk-6.5-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/oauth2-oidc-sdk-6.5-sources.jar new file mode 100644 index 000000000..5d7081296 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/oauth2-oidc-sdk-6.5-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/okhttp-3.12.6-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/okhttp-3.12.6-sources.jar new file mode 100644 index 000000000..c3e876d95 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/okhttp-3.12.6-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/okhttp-urlconnection-3.12.2-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/okhttp-urlconnection-3.12.2-sources.jar new file mode 100644 index 000000000..dfb5896ac Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/okhttp-urlconnection-3.12.2-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/okio-1.15.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/okio-1.15.0-sources.jar new file mode 100644 index 000000000..4a98ffcc1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/okio-1.15.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/org.jacoco.agent-0.8.4-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/org.jacoco.agent-0.8.4-sources.jar new file mode 100644 index 000000000..5fb362c06 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/org.jacoco.agent-0.8.4-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/retrofit-2.5.0-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/retrofit-2.5.0-sources.jar new file mode 100644 index 000000000..e8b0468bc Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/retrofit-2.5.0-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/rhino-1.7.7.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/rhino-1.7.7.1-sources.jar new file mode 100644 index 000000000..e7bbd2a63 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/rhino-1.7.7.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/rxjava-1.3.8-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/rxjava-1.3.8-sources.jar new file mode 100644 index 000000000..1bd9193ba Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/rxjava-1.3.8-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/slf4j-api-1.7.21-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/slf4j-api-1.7.21-sources.jar new file mode 100644 index 000000000..c8d239e89 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/slf4j-api-1.7.21-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/stax-ex-1.8.1-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/stax-ex-1.8.1-sources.jar new file mode 100644 index 000000000..fe66c0533 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/stax-ex-1.8.1-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/txw2-2.3.2-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/txw2-2.3.2-sources.jar new file mode 100644 index 000000000..04ea3a272 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/txw2-2.3.2-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/uri-template-0.9-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/uri-template-0.9-sources.jar new file mode 100644 index 000000000..cdadd83ca Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib-src/uri-template-0.9-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/FastInfoset-1.2.16.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/FastInfoset-1.2.16.jar new file mode 100644 index 000000000..6e91f1cec Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/FastInfoset-1.2.16.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/accessors-smart-1.2.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/accessors-smart-1.2.jar new file mode 100644 index 000000000..f4505e91d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/accessors-smart-1.2.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/activation-1.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/activation-1.1.jar new file mode 100644 index 000000000..53f82a1c4 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/activation-1.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/adal4j-1.6.4.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/adal4j-1.6.4.jar new file mode 100644 index 000000000..8e273e31a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/adal4j-1.6.4.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/adapter-rxjava-2.6.2.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/adapter-rxjava-2.6.2.jar new file mode 100644 index 000000000..3907de2fc Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/adapter-rxjava-2.6.2.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/animal-sniffer-annotations-1.18.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/animal-sniffer-annotations-1.18.jar new file mode 100644 index 000000000..392527325 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/animal-sniffer-annotations-1.18.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/applicationinsights-core-2.5.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/applicationinsights-core-2.5.1.jar new file mode 100644 index 000000000..edaab934d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/applicationinsights-core-2.5.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/asm-5.0.4.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/asm-5.0.4.jar new file mode 100644 index 000000000..cdb283dd7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/asm-5.0.4.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-1.30.0.jar new file mode 100644 index 000000000..c8eca49c2 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-annotations-1.7.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-annotations-1.7.0.jar new file mode 100644 index 000000000..465f41d9b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-annotations-1.7.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-arm-client-runtime-1.6.5.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-arm-client-runtime-1.6.5.jar new file mode 100644 index 000000000..003eaaabe Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-arm-client-runtime-1.6.5.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-auth-helper-0.2.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-auth-helper-0.2.0.jar new file mode 100644 index 000000000..46b32936b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-auth-helper-0.2.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-client-authentication-1.6.15.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-client-authentication-1.6.15.jar new file mode 100644 index 000000000..fbaca8819 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-client-authentication-1.6.15.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-client-runtime-1.7.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-client-runtime-1.7.0.jar new file mode 100644 index 000000000..884e0af20 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-client-runtime-1.7.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-keyvault-1.0.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-keyvault-1.0.0.jar new file mode 100644 index 000000000..684c48000 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-keyvault-1.0.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-keyvault-core-0.8.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-keyvault-core-0.8.0.jar new file mode 100644 index 000000000..bc5e3ab6d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-keyvault-core-0.8.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-keyvault-webkey-1.0.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-keyvault-webkey-1.0.0.jar new file mode 100644 index 000000000..82945c392 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-keyvault-webkey-1.0.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-maven-plugin-common-0.1.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-maven-plugin-common-0.1.0.jar new file mode 100644 index 000000000..84d2912b6 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-maven-plugin-common-0.1.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-appplatform-1.0.0-beta-2.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-appplatform-1.0.0-beta-2.jar new file mode 100644 index 000000000..38ad35ac0 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-appplatform-1.0.0-beta-2.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-appservice-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-appservice-1.30.0.jar new file mode 100644 index 000000000..d979fb033 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-appservice-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-batch-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-batch-1.30.0.jar new file mode 100644 index 000000000..3dd8af2c8 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-batch-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-batchai-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-batchai-1.30.0.jar new file mode 100644 index 000000000..d3b9f77c6 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-batchai-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-cdn-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-cdn-1.30.0.jar new file mode 100644 index 000000000..5cf27e4da Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-cdn-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-compute-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-compute-1.30.0.jar new file mode 100644 index 000000000..c404f343a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-compute-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-containerinstance-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-containerinstance-1.30.0.jar new file mode 100644 index 000000000..a0bd64795 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-containerinstance-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-containerregistry-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-containerregistry-1.30.0.jar new file mode 100644 index 000000000..3a4ceac57 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-containerregistry-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-containerservice-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-containerservice-1.30.0.jar new file mode 100644 index 000000000..a7a0e29fe Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-containerservice-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-cosmosdb-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-cosmosdb-1.30.0.jar new file mode 100644 index 000000000..7db809979 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-cosmosdb-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-dns-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-dns-1.30.0.jar new file mode 100644 index 000000000..360d1319b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-dns-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-eventhub-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-eventhub-1.30.0.jar new file mode 100644 index 000000000..9055021a7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-eventhub-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-graph-rbac-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-graph-rbac-1.30.0.jar new file mode 100644 index 000000000..c7cdf4696 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-graph-rbac-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-keyvault-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-keyvault-1.30.0.jar new file mode 100644 index 000000000..68b04ce0b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-keyvault-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-locks-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-locks-1.30.0.jar new file mode 100644 index 000000000..46107f0ff Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-locks-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-monitor-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-monitor-1.30.0.jar new file mode 100644 index 000000000..356d8bf49 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-monitor-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-msi-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-msi-1.30.0.jar new file mode 100644 index 000000000..146a3e651 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-msi-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-network-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-network-1.30.0.jar new file mode 100644 index 000000000..630817c82 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-network-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-redis-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-redis-1.30.0.jar new file mode 100644 index 000000000..1b1628924 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-redis-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-resources-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-resources-1.30.0.jar new file mode 100644 index 000000000..43e4ba5ff Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-resources-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-search-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-search-1.30.0.jar new file mode 100644 index 000000000..ec7ee9290 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-search-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-servicebus-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-servicebus-1.30.0.jar new file mode 100644 index 000000000..2c694bc7d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-servicebus-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-sql-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-sql-1.30.0.jar new file mode 100644 index 000000000..6c6dbd21d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-sql-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-storage-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-storage-1.30.0.jar new file mode 100644 index 000000000..411bd2b5e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-storage-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-trafficmanager-1.30.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-trafficmanager-1.30.0.jar new file mode 100644 index 000000000..7cd3d1bbf Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-mgmt-trafficmanager-1.30.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-storage-6.1.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-storage-6.1.0.jar new file mode 100644 index 000000000..a6083872d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/azure-storage-6.1.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/btf-1.2.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/btf-1.2.jar new file mode 100644 index 000000000..bbeee889b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/btf-1.2.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/checker-qual-2.8.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/checker-qual-2.8.1.jar new file mode 100644 index 000000000..09269be6a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/checker-qual-2.8.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/client-runtime-1.7.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/client-runtime-1.7.0.jar new file mode 100644 index 000000000..cbb27661e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/client-runtime-1.7.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-codec-1.11.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-codec-1.11.jar new file mode 100644 index 000000000..22451206d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-codec-1.11.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-collections4-4.4.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-collections4-4.4.jar new file mode 100644 index 000000000..da06c3e4b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-collections4-4.4.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-io-2.6.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-io-2.6.jar new file mode 100644 index 000000000..00556b119 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-io-2.6.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-lang3-3.9.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-lang3-3.9.jar new file mode 100644 index 000000000..0d8969392 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-lang3-3.9.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-logging-1.2.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-logging-1.2.jar new file mode 100644 index 000000000..93a3b9f6d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/commons-logging-1.2.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/converter-jackson-2.5.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/converter-jackson-2.5.0.jar new file mode 100644 index 000000000..e92ec6bb9 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/converter-jackson-2.5.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/error_prone_annotations-2.3.2.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/error_prone_annotations-2.3.2.jar new file mode 100644 index 000000000..bc2584db8 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/error_prone_annotations-2.3.2.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/failureaccess-1.0.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/failureaccess-1.0.1.jar new file mode 100644 index 000000000..9b56dc751 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/failureaccess-1.0.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/hamcrest-core-1.3.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/hamcrest-core-1.3.jar new file mode 100644 index 000000000..9d5fe16e3 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/hamcrest-core-1.3.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/httpclient-4.5.9.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/httpclient-4.5.9.jar new file mode 100644 index 000000000..83bc29dbe Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/httpclient-4.5.9.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/httpcore-4.4.11.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/httpcore-4.4.11.jar new file mode 100644 index 000000000..c31d40143 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/httpcore-4.4.11.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/istack-commons-runtime-3.0.8.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/istack-commons-runtime-3.0.8.jar new file mode 100644 index 000000000..8f37e950b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/istack-commons-runtime-3.0.8.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/j2objc-annotations-1.3.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/j2objc-annotations-1.3.jar new file mode 100644 index 000000000..a429c7219 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/j2objc-annotations-1.3.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-annotations-2.9.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-annotations-2.9.0.jar new file mode 100644 index 000000000..c602d75d4 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-annotations-2.9.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-core-2.9.4.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-core-2.9.4.jar new file mode 100644 index 000000000..5971a55a0 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-core-2.9.4.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-coreutils-1.9.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-coreutils-1.9.jar new file mode 100644 index 000000000..d74f5c5a0 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-coreutils-1.9.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-databind-2.9.6.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-databind-2.9.6.jar new file mode 100644 index 000000000..e8eb65867 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-databind-2.9.6.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-datatype-joda-2.10.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-datatype-joda-2.10.0.jar new file mode 100644 index 000000000..bf63aa7ad Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jackson-datatype-joda-2.10.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jakarta.activation-api-1.2.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jakarta.activation-api-1.2.1.jar new file mode 100644 index 000000000..bbfb52ff0 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jakarta.activation-api-1.2.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jakarta.xml.bind-api-2.3.2.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jakarta.xml.bind-api-2.3.2.jar new file mode 100644 index 000000000..b16236d56 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jakarta.xml.bind-api-2.3.2.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jansi-1.17.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jansi-1.17.1.jar new file mode 100644 index 000000000..7a04257d2 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jansi-1.17.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/javax.mail-1.6.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/javax.mail-1.6.1.jar new file mode 100644 index 000000000..2ecc1edf1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/javax.mail-1.6.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/javax.servlet-api-3.1.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/javax.servlet-api-3.1.0.jar new file mode 100644 index 000000000..6b14c3d26 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/javax.servlet-api-3.1.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jaxb-api-2.3.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jaxb-api-2.3.0.jar new file mode 100644 index 000000000..0817c083a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jaxb-api-2.3.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jaxb-runtime-2.3.2.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jaxb-runtime-2.3.2.jar new file mode 100644 index 000000000..62f871966 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jaxb-runtime-2.3.2.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jcip-annotations-1.0-1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jcip-annotations-1.0-1.jar new file mode 100644 index 000000000..edfda762f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jcip-annotations-1.0-1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-http-9.4.20.v20190813.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-http-9.4.20.v20190813.jar new file mode 100644 index 000000000..17136c015 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-http-9.4.20.v20190813.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-io-9.4.20.v20190813.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-io-9.4.20.v20190813.jar new file mode 100644 index 000000000..a6c4610c7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-io-9.4.20.v20190813.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-security-9.4.20.v20190813.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-security-9.4.20.v20190813.jar new file mode 100644 index 000000000..6065de72e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-security-9.4.20.v20190813.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-server-9.4.20.v20190813.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-server-9.4.20.v20190813.jar new file mode 100644 index 000000000..bc19b8208 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-server-9.4.20.v20190813.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-servlet-9.4.20.v20190813.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-servlet-9.4.20.v20190813.jar new file mode 100644 index 000000000..7d0b6893d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-servlet-9.4.20.v20190813.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-util-9.4.20.v20190813.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-util-9.4.20.v20190813.jar new file mode 100644 index 000000000..d6cc16f61 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jetty-util-9.4.20.v20190813.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jjwt-0.9.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jjwt-0.9.1.jar new file mode 100644 index 000000000..905c80e9f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jjwt-0.9.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/joda-time-2.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/joda-time-2.1.jar new file mode 100644 index 000000000..b2aca95bb Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/joda-time-2.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jopt-simple-5.0.3.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jopt-simple-5.0.3.jar new file mode 100644 index 000000000..39a78d0e1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jopt-simple-5.0.3.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/json-schema-core-1.2.10.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/json-schema-core-1.2.10.jar new file mode 100644 index 000000000..a1d727ff9 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/json-schema-core-1.2.10.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/json-schema-validator-2.2.10.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/json-schema-validator-2.2.10.jar new file mode 100644 index 000000000..8734cc5d8 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/json-schema-validator-2.2.10.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/json-smart-2.3.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/json-smart-2.3.jar new file mode 100644 index 000000000..0cd52ea4b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/json-smart-2.3.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jsr305-3.0.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jsr305-3.0.1.jar new file mode 100644 index 000000000..021df892b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/jsr305-3.0.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/junit-4.12.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/junit-4.12.jar new file mode 100644 index 000000000..3a7fc266c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/junit-4.12.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/lang-tag-1.4.4.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/lang-tag-1.4.4.jar new file mode 100644 index 000000000..46e54f789 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/lang-tag-1.4.4.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/libphonenumber-8.0.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/libphonenumber-8.0.0.jar new file mode 100644 index 000000000..8fdb51038 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/libphonenumber-8.0.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar new file mode 100644 index 000000000..45832c052 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/logging-interceptor-3.12.2.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/logging-interceptor-3.12.2.jar new file mode 100644 index 000000000..f5e5417ab Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/logging-interceptor-3.12.2.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/mailapi-1.4.3.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/mailapi-1.4.3.jar new file mode 100644 index 000000000..aeb77112b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/mailapi-1.4.3.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/msg-simple-1.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/msg-simple-1.1.jar new file mode 100644 index 000000000..db7421069 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/msg-simple-1.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/nimbus-jose-jwt-8.4.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/nimbus-jose-jwt-8.4.jar new file mode 100644 index 000000000..26e033c11 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/nimbus-jose-jwt-8.4.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/nimbus-jose-jwt-8.6.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/nimbus-jose-jwt-8.6.jar new file mode 100644 index 000000000..2984a0c60 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/nimbus-jose-jwt-8.6.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/oauth2-oidc-sdk-6.5.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/oauth2-oidc-sdk-6.5.jar new file mode 100644 index 000000000..ce943fe1f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/oauth2-oidc-sdk-6.5.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/okhttp-3.12.6.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/okhttp-3.12.6.jar new file mode 100644 index 000000000..dc836d74b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/okhttp-3.12.6.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/okhttp-urlconnection-3.12.2.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/okhttp-urlconnection-3.12.2.jar new file mode 100644 index 000000000..a360c0a4d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/okhttp-urlconnection-3.12.2.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/okio-1.15.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/okio-1.15.0.jar new file mode 100644 index 000000000..ab8ab7304 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/okio-1.15.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/org.jacoco.agent-0.8.4-runtime.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/org.jacoco.agent-0.8.4-runtime.jar new file mode 100644 index 000000000..1a9e96dbf Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/org.jacoco.agent-0.8.4-runtime.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/retrofit-2.5.0.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/retrofit-2.5.0.jar new file mode 100644 index 000000000..13dce3c49 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/retrofit-2.5.0.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/rhino-1.7.7.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/rhino-1.7.7.1.jar new file mode 100644 index 000000000..a8b9417a3 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/rhino-1.7.7.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/rxjava-1.3.8.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/rxjava-1.3.8.jar new file mode 100644 index 000000000..3b8b9e479 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/rxjava-1.3.8.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/slf4j-api-1.7.21.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/slf4j-api-1.7.21.jar new file mode 100644 index 000000000..2a5c33ec5 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/slf4j-api-1.7.21.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/stax-ex-1.8.1.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/stax-ex-1.8.1.jar new file mode 100644 index 000000000..a200db532 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/stax-ex-1.8.1.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/txw2-2.3.2.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/txw2-2.3.2.jar new file mode 100644 index 000000000..0d5ac012d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/txw2-2.3.2.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/uri-template-0.9.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/uri-template-0.9.jar new file mode 100644 index 000000000..fa50197b3 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/lib/uri-template-0.9.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/plugin.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/plugin.xml new file mode 100755 index 000000000..b2c271481 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/plugin.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/pom.xml new file mode 100755 index 000000000..551fd694c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + org.springframework.boot.ide + org.springframework.boot.ide + 4.8.1-SNAPSHOT + ../../eclipse-distribution/pom.xml + + + org.springframework.ide.eclipse + org.springframework.ide.eclipse.boot.dash.azure + eclipse-plugin + + + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + second-generate-p2-metadata + + p2-metadata + + verify + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/BootDashAzurePlugin.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/BootDashAzurePlugin.java new file mode 100644 index 000000000..d6c59e1d0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/BootDashAzurePlugin.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2020 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: + * Kris De Volder - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.azure; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +public class BootDashAzurePlugin extends AbstractUIPlugin { + + private static final String PLUGIN_ID = "org.springframework.ide.eclipse.boot.dash.azure"; + + private static final String AZURE_ICON = "azure-icon"; + +// @Override +// protected void initializeImageRegistry(ImageRegistry reg) { +// super.initializeImageRegistry(reg); +// reg.put(AZURE_ICON, getImageDescriptor("/icons/azure.png")); +// } + + /** + * Returns an image descriptor for the image file at the given plug-in + * relative path + * + * @param path + * the path + * @return the image descriptor + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromPlugin(PLUGIN_ID, path); + } + + @Override + public void start(BundleContext context) throws Exception { + System.out.println("BootDashAzurePlugin starting..."); + super.start(context); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/BootDashInjections.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/BootDashInjections.java new file mode 100644 index 000000000..779c232d1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/BootDashInjections.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2020 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.azure; + +import org.springframework.ide.eclipse.boot.dash.di.EclipseBeanLoader.Contribution; +import org.springframework.ide.eclipse.boot.dash.azure.runtarget.AzureRunTargetType; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.DefaultBootDashModelContext; + +/** + * Contributes bean definitions to {@link DefaultBootDashModelContext} + */ +public class BootDashInjections implements Contribution { + @Override + public void applyBeanDefinitions(SimpleDIContext context) throws Exception { + //TargetType + context.def(AzureRunTargetType.class, AzureRunTargetType::new); + + //UI actions +// context.defInstance(BootDashActions.Factory.class, AzureBootDashActions.factory); + + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/client/STSAzureClient.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/client/STSAzureClient.java new file mode 100644 index 000000000..76294bfbb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/client/STSAzureClient.java @@ -0,0 +1,172 @@ +package org.springframework.ide.eclipse.boot.dash.azure.client; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.core.runtime.Assert; +import org.springframework.ide.eclipse.boot.dash.azure.runtarget.AzureTargetParams; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.microsoft.azure.AzureEnvironment; +import com.microsoft.azure.PagedList; +import com.microsoft.azure.auth.AzureAuthHelper; +import com.microsoft.azure.auth.AzureCredential; +import com.microsoft.azure.credentials.AzureTokenCredentials; +import com.microsoft.azure.management.Azure; +import com.microsoft.azure.management.Azure.Authenticated; +import com.microsoft.azure.management.appplatform.v2019_05_01_preview.implementation.ServiceResourceInner; +import com.microsoft.azure.management.resources.Subscription; + +public class STSAzureClient { + + /** + * Informations identifying a user and their credentials. These are not used directly + * to access the api. Instead they are used to obtain 'authTokens'. + */ + private AzureCredential credentials; + + /** + * Credentials are used to obtain oauthTokens: + */ + private AzureTokenCredentials authTokens; + + /** + * The azure client for get list of subscriptions (i.e. this client is not targetted to + * a specific spring cloud cluster (called a 'service' in Azure speak. This is the equivalent + * of a CF space (i.e. a place in which apps can be deployed). So this client is + * authenticated to a specific user but not targeted to any particular subscription/group/service. + */ + private Authenticated azure; + + protected SpringServiceClient springServiceClient; + + /** + * Set to a specific subscription once it has been selected. Set to null otherwise. + */ + private Subscription subscription; + + private ServiceResourceInner cluster; + + public void authenticate() throws Exception { + getAuthTokens(); + this.azure = Azure.authenticate(authTokens); + if (azure==null) { + throw new IOException("Couldn't obtain credentials"); + } + } + + private void getAuthTokens() throws Exception { + final AzureEnvironment environment = AzureEnvironment.AZURE; +// try { + if (this.credentials==null) { + this.credentials = AzureAuthHelper.oAuthLogin(environment); + } + + // We can't use the deviceLogin helper as is because it prints to sysout to interact with the user + // and instructing them to get paste a auth code into a browser. +// } catch (DesktopNotSupportedException e) { +// this.credentials = AzureAuthHelper.deviceLogin(environment); +// } + authTokens = AzureAuthHelper.getMavenAzureLoginCredentials(credentials, environment); + } + + public SpringServiceClient getSpringServiceClient() { + if (springServiceClient == null) { + springServiceClient = new SpringServiceClient(authTokens, subscription.subscriptionId(), getUserAgent()); + } + return springServiceClient; + } + + private String getUserAgent() { + return "spring-tool-suite/4.1.5-testing"; + } + + +// public STSAzureClient connect(UserInteractions ui) throws Exception { +// authenticate(); +// selectSub(ui); +// selectAppCluster(); +// +// return this; +// } + +// private void selectSub(UserInteractions ui) { +// PagedList subs = azure.subscriptions().list(); +// if (subs.isEmpty()) { +// ui.errorPopup("No Azure Subscriptions Found", "You need an Azure Subscription, but none was " +// + "found to be associated with your user. Please sign up for an Azure subscription or " +// + "try again to login as a different user"); +// return; +// } +// +// subs.forEach(subscription -> { +// System.out.println("Subscription: "+subscription.displayName()); +// System.out.println(" Id: "+subscription.subscriptionId()); +// azure.withSubscription(subscription.subscriptionId()); +// System.out.println(subscription); +// }); +// //TODO: +//// if (subs.size()>1) { +//// choose one +//// } else { +// this.sub = subs.get(0); +//// } +// } + + /** + * Connect to azure with credentials obtained by prompting the user + */ + static public boolean login(UserInteractions ui) { + try { + if (ui.confirmOperation("Obtaining Credentials via OAuth", + "To access your Azure Spring Cloud subscriptions, resources and services, " + + "STS needs to be authorized by you. A web browser will now be opened for that purpose.") + ) { + STSAzureClient client = new STSAzureClient(); + client.authenticate(); + return true; + } + } catch (Exception e) { + Log.log(e); + ui.errorPopup("Authentication failed", ExceptionUtil.getMessage(e)); + } + return false; + } + + public PagedList getSubsriptions() { + Assert.isLegal(azure!=null, "Not authenticated (call login or connect befor calling this method)"); + return azure.subscriptions().list(); + } + + public void setSubscription(Subscription sub) { + this.subscription = sub; + } + + public void setCluster(ServiceResourceInner cluster) { + this.cluster = cluster; + } + + public AzureTargetParams getTargetParams() { + return new AzureTargetParams( + this.credentials, + subscription.subscriptionId(), + subscription.displayName(), + cluster.id(), + cluster.name() + ); + } + + public void reconnect(AzureTargetParams params) throws Exception { + this.credentials = params.getCredentials(); + this.authenticate(); + this.setSubscription(azure.subscriptions().getById(params.getSubscriptionId())); + this.setCluster(getSpringServiceClient().getClusterById(params.getClusterId())); + } + + private Subscription getSubsription(String subscriptionId) { + return azure.subscriptions().getById(subscriptionId); + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/client/SpringServiceClient.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/client/SpringServiceClient.java new file mode 100644 index 000000000..452d52cce --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/client/SpringServiceClient.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) Microsoft Corporation and others. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ +package org.springframework.ide.eclipse.boot.dash.azure.client; + +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.ide.eclipse.boot.dash.azure.runtarget.AzureRunTargetType; +import org.springframework.ide.eclipse.boot.dash.azure.runtarget.AzureTargetParams; + +import com.microsoft.azure.PagedList; +import com.microsoft.azure.credentials.AzureTokenCredentials; +import com.microsoft.azure.management.appplatform.v2019_05_01_preview.implementation.AppPlatformManager; +import com.microsoft.azure.management.appplatform.v2019_05_01_preview.implementation.ServiceResourceInner; +import com.microsoft.rest.LogLevel; + +public class SpringServiceClient { + + protected static final String NO_CLUSTER = "No cluster named %s found in subscription %s"; + + private String subscriptionId; + private AppPlatformManager springManager; + + public SpringServiceClient(AzureTokenCredentials azureTokenCredentials, String subscriptionId, String userAgent) { + this(azureTokenCredentials, subscriptionId, userAgent, LogLevel.NONE); + } + + public SpringServiceClient(AzureTokenCredentials azureTokenCredentials, String subscriptionId, String userAgent, LogLevel logLevel) { + subscriptionId = StringUtils.isEmpty(subscriptionId) ? azureTokenCredentials.defaultSubscriptionId() : subscriptionId; + this.subscriptionId = subscriptionId; + this.springManager = AppPlatformManager.configure() + .withLogLevel(logLevel) + .withUserAgent(userAgent) + .authenticate(azureTokenCredentials, subscriptionId); + } + +// public SpringAppClient newSpringAppClient(String subscriptionId, String cluster, String app) { +// final SpringAppClient.Builder builder = new SpringAppClient.Builder(); +// return builder.withSubscriptionId(subscriptionId) +// .withSpringServiceClient(this) +// .withClusterName(cluster) +// .withAppName(app) +// .build(); +// } +// +// public SpringAppClient newSpringAppClient(SpringConfiguration configuration) { +// return newSpringAppClient(configuration.getSubscriptionId(), configuration.getClusterName(), configuration.getAppName()); +// } + + public ServiceResourceInner getClusterById(String id) { + String resourceGroupName = AzureRunTargetType.getResourceGroupName(id); + String serviceName = AzureRunTargetType.getServiceName(id); + + return getSpringManager().inner().services() + .getByResourceGroup(resourceGroupName, serviceName); + } + + public List getAvailableClusters() { + final PagedList clusterList = getSpringManager().inner().services().list(); + clusterList.loadAll(); + return new ArrayList<>(clusterList); + } + + public ServiceResourceInner getClusterByName(String cluster) { + final List clusterList = getAvailableClusters(); + return clusterList.stream().filter(appClusterResourceInner -> appClusterResourceInner.name().equals(cluster)) + .findFirst() + .orElseThrow(() -> new InvalidParameterException(String.format(NO_CLUSTER, cluster, subscriptionId))); + } + + public String getResourceGroupByCluster(String clusterName) { + final ServiceResourceInner cluster = getClusterByName(clusterName); + final String[] attributes = cluster.id().split("/"); + return attributes[ArrayUtils.indexOf(attributes, "resourceGroups") + 1]; + } + + public String getSubscriptionId() { + return subscriptionId; + } + + public AppPlatformManager getSpringManager() { + return springManager; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureApp.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureApp.java new file mode 100644 index 000000000..82107dec5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureApp.java @@ -0,0 +1,75 @@ +package org.springframework.ide.eclipse.boot.dash.azure.runtarget; + +import java.util.Set; + +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.AppContext; +import org.springframework.ide.eclipse.boot.dash.api.RunStateProvider; +import org.springframework.ide.eclipse.boot.dash.azure.client.SpringServiceClient; +import org.springframework.ide.eclipse.boot.dash.model.RunState; + +import com.google.common.collect.ImmutableSet; +import com.microsoft.azure.management.appplatform.v2019_05_01_preview.AppResource; +import com.microsoft.azure.management.appplatform.v2019_05_01_preview.DeploymentResource; +import com.microsoft.azure.management.appplatform.v2019_05_01_preview.DeploymentResourceProvisioningState; + +public class AzureApp implements App, RunStateProvider { + + public static final Set BUSY = ImmutableSet.of( + DeploymentResourceProvisioningState.CREATING, + DeploymentResourceProvisioningState.UPDATING + ); + + private AzureRunTarget target; + private AppResource app; + + public AzureApp(AzureRunTarget azureRunTarget, AppResource appResource) { + this.target = azureRunTarget; + this.app = appResource; + } + + @Override + public String getName() { + return app.name(); + } + + @Override + public RunState fetchRunState() { + String rg = target.getResourceGroupName(); + String sn = target.getClusterName(); + SpringServiceClient client = target.getClient(); + if (client!=null) { + String activeDepName = app.properties().activeDeploymentName(); + if (activeDepName!=null) { + DeploymentResource dep = client.getSpringManager().deployments().getAsync(rg, sn, app.name(), activeDepName).toBlocking().single(); + if (dep!=null) { + DeploymentResourceProvisioningState state = dep.properties().provisioningState(); + if (BUSY.contains(state)) { + return RunState.STARTING; + } else if (DeploymentResourceProvisioningState.SUCCEEDED.equals(state)) { + if (dep.properties().active()) { + return RunState.RUNNING; + } + } else if (DeploymentResourceProvisioningState.FAILED.equals(state)) { + return RunState.CRASHED; + } else { + return RunState.UNKNOWN; + } + } + } + return RunState.INACTIVE; + } + return RunState.UNKNOWN; + } + + @Override + public AzureRunTarget getTarget() { + return target; + } + + @Override + public void setContext(AppContext context) { + // TODO Auto-generated method stub + + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureRunTarget.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureRunTarget.java new file mode 100644 index 000000000..c9e578ba8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureRunTarget.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * Copyright (c) 2020 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.azure.runtarget; + +import java.util.Collection; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.azure.BootDashAzurePlugin; +import org.springframework.ide.eclipse.boot.dash.azure.client.STSAzureClient; +import org.springframework.ide.eclipse.boot.dash.azure.client.SpringServiceClient; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.RemoteBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.AbstractRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTarget; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; + +import com.google.common.collect.ImmutableSet; +import com.microsoft.azure.management.appplatform.v2019_05_01_preview.AppResource; + +public class AzureRunTarget extends AbstractRunTarget implements RemoteRunTarget { + + private final AzureTargetParams params; + private final LiveVariable client = new LiveVariable<>(); + + @Override + public LiveExpression getClientExp() { + return client; + } + + /** + * Creates a target in 'connected' state. + */ + public AzureRunTarget(AzureRunTargetType type, STSAzureClient client) { + this(type, client.getTargetParams()); + SpringServiceClient connection = client.getSpringServiceClient(); + this.client.setValue(connection); + } + + /** + * Create a target in a not connected state, but with all the info needed to + * estabslish a connection (at a later time). + */ + public AzureRunTarget(AzureRunTargetType type, AzureTargetParams properties) { + super(type, properties.getClusterId(), properties.getClusterName()); + this.params = properties; + } + + @Override + public RemoteBootDashModel createSectionModel(BootDashViewModel parent) { + return new GenericRemoteBootDashModel<>(this, parent); + } + + @Override + public boolean canRemove() { + return true; + } + + @Override + public boolean canDeployAppsFrom() { + return false; + } + + @Override + public AzureTargetParams getParams() { + return params; + } + + @Override + public void dispose() { + } + + @Override + public String getDisplayName() { + return getResourceGroupName() + " : "+getClusterName() + " ["+getSubscriptionName()+"]"; + } + + private String getSubscriptionName() { + return params.getSubscriptionName(); + } + + String getClusterName() { + return params.getClusterName(); + } + + String getResourceGroupName() { + String clusterId = params.getClusterId(); + return AzureRunTargetType.getResourceGroupName(clusterId); + } + + @Override + public Collection fetchApps() { + SpringServiceClient client = this.getClient(); + if (client!=null) { + String resourceGroupName = getResourceGroupName(); + String serviceName = getClusterName(); + Iterable apps = client.getSpringManager().apps().listAsync(resourceGroupName, serviceName).toBlocking().toIterable(); + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (AppResource appResource : apps) { + System.out.println(appResource); + System.out.println(appResource.properties().provisioningState()); + builder.add(new AzureApp(this, appResource)); + } + return builder.build(); + } + return ImmutableSet.of(); + } + + @Override + public void disconnect() { + //SpringServiceClient c = client.getValue(); + client.setValue(null); + } + + @Override + public void connect(ConnectMode mode) throws Exception { + STSAzureClient c = new STSAzureClient(); + c.reconnect(getParams()); + client.setValue(c.getSpringServiceClient()); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureRunTargetType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureRunTargetType.java new file mode 100644 index 000000000..89b5fd56d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureRunTargetType.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * Copyright (c) 2020 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.azure.runtarget; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jface.resource.ImageDescriptor; +import org.springframework.ide.eclipse.boot.dash.azure.BootDashAzurePlugin; +import org.springframework.ide.eclipse.boot.dash.azure.client.STSAzureClient; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.AbstractRemoteRunTargetType; +import org.springsource.ide.eclipse.commons.frameworks.core.util.JobUtil; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.gson.Gson; +import com.microsoft.azure.PagedList; +import com.microsoft.azure.management.appplatform.v2019_05_01_preview.implementation.ServiceResourceInner; +import com.microsoft.azure.management.resources.Subscription; + +public class AzureRunTargetType extends AbstractRemoteRunTargetType { + + public static String getResourceGroupName(String serviceId) { + // Example clusterId="/subscriptions/9036e83e-2238-42a4-9b2a-ecd80d4cc38d/resourceGroups/resource-test-dc/providers/Microsoft.AppPlatform/Spring/piggymetrics" + String[] parts = StringUtils.splitPreserveAllTokens(serviceId, '/'); + return parts[4]; + } + + public static String getServiceName(String serviceId) { + // Example clusterId="/subscriptions/9036e83e-2238-42a4-9b2a-ecd80d4cc38d/resourceGroups/resource-test-dc/providers/Microsoft.AppPlatform/Spring/piggymetrics" + String[] parts = StringUtils.splitPreserveAllTokens(serviceId, '/'); + return parts[8]; + } + + + public AzureRunTargetType(SimpleDIContext injections) { + super(injections, "Azure Spring Cloud"); + } + + @Override + public CompletableFuture openTargetCreationUi(LiveSetVariable targets) { + return JobUtil.runInJob("Azure Target Creation", mon -> { + STSAzureClient client = login(ui()); + if (client!=null) { + Subscription sub = chooseSubscription(client); + if (sub!=null) { + client.setSubscription(sub); + List clusters = client.getSpringServiceClient().getAvailableClusters(); + if (clusters.isEmpty()) { + ui().errorPopup("No Azure Spring Cloud Clusters", + "We did not find any existing Azure Spring Cloud Clusters under subscription " + + sub.displayName()+".\n\n" + + "Please first create a cluster using 'az' CLI and then try again" + ); + } else { + ServiceResourceInner cluster = ui().chooseElement("Choose a cluster", "Choose a cluster", clusters, c -> { + return getResourceGroupName(c.id()) +" / " + c.name(); + }); + if (cluster!=null) { + client.setCluster(cluster); + targets.add(new AzureRunTarget(this, client)); + } + } + } + } + }); + } + + private Subscription chooseSubscription(STSAzureClient client) { + PagedList subs = client.getSubsriptions(); + if (subs.isEmpty()) { + ui().errorPopup("No Azure Subscriptions Found", "You need an Azure Subscription, but none was " + + "found for the authenticated user. Please sign up for an Azure subscription or " + + "try again and authenticate as a different user."); + } else { + return ui().chooseElement("Choose a subscription", "Choose a subscription", subs, Subscription::displayName); + } + return null; + } + + @Override + public RunTarget createRunTarget(AzureTargetParams properties) { + return new AzureRunTarget(this, properties); + } + + @Override + public ImageDescriptor getIcon() { + return BootDashAzurePlugin.getImageDescriptor("/icons/azure.png"); + } + + @Override + public ImageDescriptor getDisconnectedIcon() { + return BootDashAzurePlugin.getImageDescriptor("icons/azure-inactive.png"); + } + + @Override + public AzureTargetParams parseParams(String serializedTargetParams) { + Gson gson = new Gson(); + return gson.fromJson(serializedTargetParams, AzureTargetParams.class); + } + + @Override + public String serialize(AzureTargetParams targetParams) { + Gson gson = new Gson(); + return gson.toJson(targetParams); + } + + /** + * Connect to azure with credentials obtained by prompting the user + * + * @return When login is successful, a client in authenticated (but not targeted) state; otherwise null. + */ + public STSAzureClient login(UserInteractions ui) { + try { + if (ui.confirmOperation("Obtaining Credentials via OAuth", + "To access your Azure Spring Cloud subscriptions, resources and services, " + + "STS needs to be authorized by you. A web browser will now be opened for that purpose.") + ) { + STSAzureClient client = new STSAzureClient(); + client.authenticate(); + return client; + } + } catch (Exception e) { + Log.log(e); + ui.errorPopup("Authentication failed", ExceptionUtil.getMessage(e)); + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureTargetParams.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureTargetParams.java new file mode 100644 index 000000000..3c41f1836 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.azure/src/org/springframework/ide/eclipse/boot/dash/azure/runtarget/AzureTargetParams.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) 2020 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.azure.runtarget; + +import com.microsoft.azure.auth.AzureCredential; + +/** + * Json serialisation friendly object that contains all the data + * we need to establish connection to an Azure Spring Cloud cluster. + */ +public class AzureTargetParams { + + private AzureCredential credentials; + private String subscriptionId; + private String subscriptionName; + private String clusterId; + private String clusterName; + + public AzureTargetParams() {} + + public AzureTargetParams(AzureCredential credentials, String subscriptionId, String subscriptionName, String clusterId, String clusterName) { + super(); + this.credentials = credentials; + this.subscriptionId = subscriptionId; + this.subscriptionName = subscriptionName; + this.clusterId = clusterId; + this.clusterName = clusterName; + } + + public AzureCredential getCredentials() { + return credentials; + } + + public void setCredentials(AzureCredential credentials) { + this.credentials = credentials; + } + + public String getSubscriptionId() { + return subscriptionId; + } + + public void setSubscriptionId(String subscriptionId) { + this.subscriptionId = subscriptionId; + } + + public String getClusterId() { + return clusterId; + } + + public void setClusterId(String clusterId) { + this.clusterId = clusterId; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getSubscriptionName() { + return this.subscriptionName; + } + + public void setSubscriptionName(String subscriptionName) { + this.subscriptionName = subscriptionName; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.classpath new file mode 100644 index 000000000..50fea4658 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.project new file mode 100644 index 000000000..ebd4c7802 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.project @@ -0,0 +1,28 @@ + + + org.springframework.ide.eclipse.boot.dash.cf + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..0c68a61dc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.settings/org.eclipse.jdt.ui.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..c743e1c07 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,59 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=false +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_type_arguments=false +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=false +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.settings/org.springframework.ide.eclipse.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.settings/org.springframework.ide.eclipse.prefs new file mode 100644 index 000000000..a12794d68 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/.settings/org.springframework.ide.eclipse.prefs @@ -0,0 +1,2 @@ +boot.validation.initialized=true +eclipse.preferences.version=1 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/META-INF/MANIFEST.MF b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/META-INF/MANIFEST.MF new file mode 100755 index 000000000..d257af2a8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/META-INF/MANIFEST.MF @@ -0,0 +1,76 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-SymbolicName: org.springframework.ide.eclipse.boot.dash.cf;singleton:=true +Bundle-Version: 4.8.1.qualifier +Bundle-Vendor: Pivotal Inc +Bundle-Name: Spring Boot Dash CF Support +Require-Bundle: org.eclipse.ui, + org.eclipse.ui.ide, + org.springframework.ide.eclipse.boot.launch, + org.springframework.ide.eclipse.boot, + org.springframework.ide.eclipse.boot.dash, + org.springsource.ide.eclipse.commons.livexp, + com.google.guava;bundle-version="15.0.0", + org.springframework.ide.eclipse.boot.wizard, + io.projectreactor.reactor-core, + org.reactivestreams.reactive-streams, + org.eclipse.core.resources, + org.eclipse.debug.core, + org.eclipse.jdt.core, + org.springsource.ide.eclipse.commons.ui, + org.eclipse.ui.views.properties.tabbed, + org.eclipse.jdt.launching, + org.eclipse.debug.ui, + org.eclipse.jdt.debug.ui, + org.springsource.ide.eclipse.commons.core, + com.google.gson, + org.eclipse.ui.console, + javax.ws.rs;bundle-version="2.0.1", + org.springframework.ide.eclipse.beans.ui.live, + org.eclipse.compare, + org.eclipse.core.filebuffers, + org.eclipse.text, + org.springsource.ide.eclipse.commons.frameworks.core, + org.apache.httpcomponents.httpclient, + org.apache.httpcomponents.httpcore, + org.yaml.snakeyaml, + org.apache.commons.lang3, + org.springframework.ide.eclipse.editor.support, + org.eclipse.jface.text, + org.springsource.ide.eclipse.commons.cloudfoundry.client.v2, + org.eclipse.core.net, + org.eclipse.equinox.security, + org.eclipse.jdt.ui, + org.eclipse.ui.genericeditor, + org.eclipse.ui.editors, + com.hierynomus.sshj;bundle-version="0.24.0", + com.jcraft.jzlib;bundle-version="1.1.3", + org.bouncycastle.bcpkix;bundle-version="1.59.0", + org.bouncycastle.bcprov;bundle-version="1.59.0", + org.springsource.ide.eclipse.commons.boot.ls, + org.springframework.tooling.cloudfoundry.manifest.ls, + org.springframework.tooling.ls.eclipse.commons +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Activator: org.springframework.ide.eclipse.boot.dash.cf.BootDashCfPlugin +Bundle-ActivationPolicy: lazy +Export-Package: org.springframework.ide.eclipse.boot.dash.cf, + org.springframework.ide.eclipse.boot.dash.cf.actions, + org.springframework.ide.eclipse.boot.dash.cf.client, + org.springframework.ide.eclipse.boot.dash.cf.client.v2, + org.springframework.ide.eclipse.boot.dash.cf.debug, + org.springframework.ide.eclipse.boot.dash.cf.deployment, + org.springframework.ide.eclipse.boot.dash.cf.dialogs, + org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel, + org.springframework.ide.eclipse.boot.dash.cf.model, + org.springframework.ide.eclipse.boot.dash.cf.packaging, + org.springframework.ide.eclipse.boot.dash.cf.routes, + org.springframework.ide.eclipse.boot.dash.cf.runtarget, + org.springframework.ide.eclipse.boot.dash.cf.ui +Bundle-ClassPath: ., + lib/spring-boot-loader-tools-1.2.3.RELEASE.jar +Automatic-Module-Name: org.springframework.ide.eclipse.boot.dash.cf +Import-Package: org.eclipse.core.runtime, + org.eclipse.core.runtime.jobs, + org.eclipse.core.runtime.preferences, + org.eclipse.osgi.util, + org.osgi.framework diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/build.properties b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/build.properties new file mode 100755 index 000000000..b59f529db --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/build.properties @@ -0,0 +1,7 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + plugin.xml,\ + icons/,\ + lib/spring-boot-loader-tools-1.2.3.RELEASE.jar diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/icons/cloud_obj.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/icons/cloud_obj.png new file mode 100644 index 000000000..7633c35fb Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/icons/cloud_obj.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/lib/spring-boot-loader-tools-1.2.3.RELEASE-sources.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/lib/spring-boot-loader-tools-1.2.3.RELEASE-sources.jar new file mode 100644 index 000000000..cbb02c57f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/lib/spring-boot-loader-tools-1.2.3.RELEASE-sources.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/lib/spring-boot-loader-tools-1.2.3.RELEASE.jar b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/lib/spring-boot-loader-tools-1.2.3.RELEASE.jar new file mode 100644 index 000000000..d1a02b37c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/lib/spring-boot-loader-tools-1.2.3.RELEASE.jar differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/plugin.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/plugin.xml new file mode 100755 index 000000000..8feaf9cc8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/plugin.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/pom.xml new file mode 100755 index 000000000..786ef8f52 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + org.springframework.boot.ide + org.springframework.boot.ide + 4.8.1-SNAPSHOT + ../../eclipse-distribution/pom.xml + + + org.springframework.ide.eclipse + org.springframework.ide.eclipse.boot.dash.cf + eclipse-plugin + + + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + second-generate-p2-metadata + + p2-metadata + + verify + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/BootDashCfPlugin.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/BootDashCfPlugin.java new file mode 100644 index 000000000..434e762b3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/BootDashCfPlugin.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2016, 2020 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: + * Phil Webb - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +public class BootDashCfPlugin extends AbstractUIPlugin { + + private static final String PLUGIN_ID = "org.springframework.ide.eclipse.boot.dash.cf"; + + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + CompletableFuture.runAsync(() -> BootDashTargetInfoSynchronizer.start()); + } + + @Override + public void stop(BundleContext context) throws Exception { + BootDashTargetInfoSynchronizer.stop(); + super.stop(context); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/BootDashInjections.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/BootDashInjections.java new file mode 100644 index 000000000..a19f412fa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/BootDashInjections.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2020 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.cf; + +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.cf.actions.CfBootDashActions; +import org.springframework.ide.eclipse.boot.dash.cf.client.CloudFoundryClientFactory; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.DefaultCloudFoundryClientFactoryV2; +import org.springframework.ide.eclipse.boot.dash.cf.debug.DebugSupport; +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshDebugSupport; +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshTunnelFactory; +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshTunnelImpl; +import org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.CloudFoundryRemoteBootAppsDataContributor; +import org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.JmxSshTunnelManager; +import org.springframework.ide.eclipse.boot.dash.cf.labels.BootDashCfLabels; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTargetType; +import org.springframework.ide.eclipse.boot.dash.cf.ui.CfUserInteractions; +import org.springframework.ide.eclipse.boot.dash.cf.ui.DefaultCfUserInteractions; +import org.springframework.ide.eclipse.boot.dash.di.EclipseBeanLoader.Contribution; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels; +import org.springframework.ide.eclipse.boot.dash.model.DefaultBootDashModelContext; +import org.springframework.ide.eclipse.boot.dash.views.BootDashActions; +import org.springsource.ide.eclipse.commons.boot.ls.remoteapps.RemoteBootAppsDataHolder; + +/** + * Contributes bean definitions to {@link DefaultBootDashModelContext} + */ +public class BootDashInjections implements Contribution { + + @Override + public void applyBeanDefinitions(SimpleDIContext context) throws Exception { + //TargetType + context.def(RunTargetType.class, CloudFoundryRunTargetType::new); + + //Auto sync of cf deployed apps with JMX over SSH tunnel with boot ls (for live hover/data features). + context.def(RemoteBootAppsDataHolder.Contributor.class, CloudFoundryRemoteBootAppsDataContributor::new); + + //UI actions + context.defInstance(BootDashActions.Factory.class, CfBootDashActions.factory); + + //BootDashLabels + context.defInstance(BootDashLabels.Contribution.class, BootDashCfLabels.jmxDecoration); + + //cf internal + context.def(CfUserInteractions.class, DefaultCfUserInteractions::new); + context.defInstance(DebugSupport.class, SshDebugSupport.INSTANCE); + context.defInstance(SshTunnelFactory.class, SshTunnelImpl::new); + context.defInstance(JmxSshTunnelManager.class, new JmxSshTunnelManager()); + context.defInstance(CloudFoundryClientFactory.class, DefaultCloudFoundryClientFactoryV2.INSTANCE); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/BootDashTargetInfoSynchronizer.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/BootDashTargetInfoSynchronizer.java new file mode 100644 index 000000000..d4a5fdb28 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/BootDashTargetInfoSynchronizer.java @@ -0,0 +1,160 @@ +/******************************************************************************* + * Copyright (c) 2020 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.cf; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.DefaultClientRequestsV2; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CfTargetsInfo; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CfTargetsInfo.Target; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CfTargetsInfo.TargetDiagnosticMessages; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryTargetProperties; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.tooling.cloudfoundry.manifest.ls.CloudFoundryManifestLanguageServer; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +import com.google.common.collect.ImmutableSet; + +/** + * Helper methods to attach listener to the BootDash model and keep editor in sync with + * all connected cloudfoundry targets. + *

+ * Usage: just call the 'start()' method once, at the first time the information is desired. + */ +@SuppressWarnings("rawtypes") +public class BootDashTargetInfoSynchronizer { + + private static AtomicBoolean started = new AtomicBoolean(false); + + public static void start() { + if (started.compareAndSet(false, true)) { + model().getRunTargets().addListener(targetsListener); + } + } + + public static void stop() { + if (started.compareAndSet(true, false)) { + updateCloudTargetsInManifestEditor(ImmutableSet.of()); + } + } + + private static final ValueListener> targetsListener = (e, v) -> { + // On target changes, add the client listener in each target so that when the client changes, a notification is sent + addClientChangeListeners(e.getValue()); + }; + + private static final ValueListener clientsChangedListener = (exp, client) -> { + if (client instanceof DefaultClientRequestsV2) { + addRefreshTokenListener((DefaultClientRequestsV2) client); + } + }; + + /** + * Add a listener to be notified when the refresh token becomes available OR + * changes + */ + static private void addRefreshTokenListener(DefaultClientRequestsV2 client) { + if (client != null && client.getRefreshTokens() != null && model() != null + && model().getRunTargets() != null) { + client.getRefreshTokens().doOnNext((token) -> + // Although the refresh token change is for ONE client (i.e. one target) + // compute cloud target information for ALL currently connected targets + // as the manifest editor is updated with the full up-to-date list of connected + // boot dash targets + updateCloudTargetsInManifestEditor(model().getRunTargets().getValue()) + ).subscribe(); + client.getRefreshTokens().doOnComplete(() -> + updateCloudTargetsInManifestEditor(model().getRunTargets().getValue()) + ).subscribe(); + } + } + + static private void updateCloudTargetsInManifestEditor(ImmutableSet value) { + Set toUpdate = value == null ? ImmutableSet.of() : value; + + CfTargetsInfo targetsInfo = asTargetsInfo(toUpdate); + CloudFoundryManifestLanguageServer.setCfTargetLoginOptions(targetsInfo); + } + + private static BootDashViewModel model() { + return BootDashActivator.getDefault().getModel(); + } + + private static void addClientChangeListeners(ImmutableSet targets) { + if (targets != null) { + for (RunTarget runTarget : targets) { + if (runTarget instanceof CloudFoundryRunTarget) { + ((CloudFoundryRunTarget) runTarget).getClientExp().addListener(clientsChangedListener); + } + } + } + } + + private static CfTargetsInfo asTargetsInfo(Collection targets) { + List collectedTargets = new ArrayList<>(); + for (RunTarget runTarget : targets) { + if (runTarget instanceof CloudFoundryRunTarget) { + + CloudFoundryRunTarget cloudFoundryRunTarget = (CloudFoundryRunTarget) runTarget; + if (cloudFoundryRunTarget.isConnected()) { + String token = cloudFoundryRunTarget.getClient().getRefreshToken(); + if (token != null) { + CloudFoundryTargetProperties properties = cloudFoundryRunTarget.getTargetProperties(); + String target = properties.getUrl(); + String org = properties.getOrganizationName(); + String space = properties.getSpaceName(); + boolean sslDisabled = properties.skipSslValidation(); + + CfTargetsInfo.Target integrationTarget = new Target(); + + integrationTarget.setApi(target); + integrationTarget.setOrg(org); + integrationTarget.setSpace(space); + integrationTarget.setSslDisabled(sslDisabled); + integrationTarget.setRefreshToken(token); + collectedTargets.add(integrationTarget); + } + } + } + } + + CfTargetsInfo targetsInfo = new CfTargetsInfo(); + targetsInfo.setCfTargets(collectedTargets); + targetsInfo.setDiagnosticMessages(getDiagnosticMessages()); + return targetsInfo ; + } + + // NOTE: using ':' to separate the "shorter" part of the message from the longer. The longer part may be shown in the UI by expanding the hover info + private static final String TARGET_SOURCE = "Boot Dashboard"; + private static final String NO_ORG_SPACE = "Boot Dashboard - No org/space selected: Verify Cloud Foundry target connection in Boot Dashboard or login via 'cf' CLI"; + // Make this a "generic" message, instead of using "Boot Dash" prefix as it shows general instructions when there are not targets + private static final String NO_TARGETS = "No Cloud Foundry targets found: Create a target in Boot Dashboard or login via 'cf' CLI"; + private static final String CONNECTION_ERROR = "Boot Dashboard - Error connecting to Cloud Foundry target: Verify network connection or that existing target has valid credentials."; + + private static TargetDiagnosticMessages getDiagnosticMessages() { + TargetDiagnosticMessages messages = new TargetDiagnosticMessages(); + messages.setConnectionError(CONNECTION_ERROR); + messages.setNoOrgSpace(NO_ORG_SPACE); + messages.setNoTargetsFound(NO_TARGETS); + messages.setTargetSource(TARGET_SOURCE); + return messages; + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/AbstractCloudAppDashElementsAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/AbstractCloudAppDashElementsAction.java new file mode 100644 index 000000000..02ba712a9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/AbstractCloudAppDashElementsAction.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.actions; + +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction.Params; + +/** + * Action for {@link CloudAppDashElement} elements only + * + * @author Alex Boyko + * + */ +public class AbstractCloudAppDashElementsAction extends AbstractBootDashElementsAction { + + public AbstractCloudAppDashElementsAction(Params params) { + super(params); + } + + @Override + public void updateVisibility() { + boolean visible = false; + if (!getSelectedElements().isEmpty()) { + visible = true; + for (BootDashElement e : getSelectedElements()) { + if (!(e instanceof CloudAppDashElement)) { + visible = false; + break; + } + } + } + setVisible(visible); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/AbstractCloudDashModelAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/AbstractCloudDashModelAction.java new file mode 100644 index 000000000..245544ad6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/AbstractCloudDashModelAction.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.actions; + +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.ui.CfUserInteractions; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashModelAction; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +/** + * Action for Cloud Foundry dash model element + * + * @author Alex Boyko + * + */ +public class AbstractCloudDashModelAction extends AbstractBootDashModelAction { + + protected AbstractCloudDashModelAction(LiveExpression section, SimpleDIContext ui) { + super(section, ui); + } + + protected CfUserInteractions cfUi() { + return context.getBean(CfUserInteractions.class); + } + + @Override + public void updateVisibility() { + this.setVisible(sectionSelection.getValue() instanceof CloudFoundryBootDashModel); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/CfBootDashActions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/CfBootDashActions.java new file mode 100644 index 000000000..491850acb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/CfBootDashActions.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 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.cf.actions; + +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveProcessCommandsExecutor; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashAction; +import org.springframework.ide.eclipse.boot.dash.views.BootDashActions; +import org.springframework.ide.eclipse.boot.dash.views.ToggleBootDashModelConnection; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction.Params; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +import com.google.common.collect.ImmutableList; + +public class CfBootDashActions { + + public static BootDashActions.Factory factory = ( + BootDashActions actions, + BootDashViewModel model, + MultiSelection selection, + LiveExpression section, + SimpleDIContext context, + LiveProcessCommandsExecutor liveProcessCmds + ) -> { + + Params defaultActionParams = new Params(actions) + .setModel(model) + .setSelection(selection) + .setContext(context) + .setLiveProcessCmds(liveProcessCmds); + + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(new RestartApplicationOnlyAction(defaultActionParams)); + builder.add(new SelectManifestAction(defaultActionParams)); + builder.add(new CloudFoundryRestartWithRemoteDevClientAction(defaultActionParams)); + builder.add(new EnableJmxSshTunnelAction(defaultActionParams)); + builder.add(new ReconnectCloudConsoleAction(defaultActionParams)); + if (section!=null) { + builder.add(new UpdatePasswordAction(section, context)); + builder.add(new OpenCloudAdminConsoleAction(section, context)); + builder.add(new CustmomizeTargetAppManagerURLAction(section, context)); + } + return builder.build(); + }; +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/CloudFoundryRestartWithRemoteDevClientAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/CloudFoundryRestartWithRemoteDevClientAction.java new file mode 100644 index 000000000..8954a87b8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/CloudFoundryRestartWithRemoteDevClientAction.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2015, 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.actions; + +import java.net.URL; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.PlatformUI; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; + +/** + * Action for starting/restarting Remove DevTools Client application + * + * @author Alex Boyko + * + */ +public class CloudFoundryRestartWithRemoteDevClientAction extends AbstractCloudAppDashElementsAction { + + private ElementStateListener stateListener; + + public CloudFoundryRestartWithRemoteDevClientAction(Params params) { + super(params); + this.setText("(Re)start Remote DevTools Client"); + this.setToolTipText("Restarts application with the Remote DevTools Client attched."); + URL url = FileLocator.find(Platform.getBundle("org.springframework.ide.eclipse.boot"), new Path("resources/icons/boot-devtools-icon.png"), null); + if (url != null) { + this.setImageDescriptor(ImageDescriptor.createFromURL(url)); + } + if (model != null) { + model.addElementStateListener(stateListener = new ElementStateListener() { + public void stateChanged(BootDashElement e) { + if (getSelectedElements().contains(e) && !PlatformUI.getWorkbench().isClosing()) { + PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { + public void run() { + updateEnablement(); + } + }); + } + } + }); + } + } + + @Override + public void updateEnablement() { + boolean enable = false; + if (!getSelectedElements().isEmpty()) { + enable = true; + for (BootDashElement e : getSelectedElements()) { + if (!(e instanceof CloudAppDashElement) || e.getProject() == null) { + enable = false; + } + } + } + this.setEnabled(enable); + } + + @Override + public void run() { + for (BootDashElement _e : getSelectedElements()) { + if (_e instanceof CloudAppDashElement && _e.getBootDashModel() instanceof CloudFoundryBootDashModel && _e.getProject() != null) { + CloudAppDashElement e = (CloudAppDashElement) _e; + e.restartWithRemoteClient(ui(), e.createCancelationToken()); + } + } + } + + @Override + public void dispose() { + if (model != null && stateListener != null) { + model.removeElementStateListener(stateListener); + stateListener = null; + } + super.dispose(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/CustmomizeTargetAppManagerURLAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/CustmomizeTargetAppManagerURLAction.java new file mode 100644 index 000000000..04e053d7c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/CustmomizeTargetAppManagerURLAction.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.actions; + +import java.util.EnumSet; + +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.CustomizeAppsManagerURLDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +public class CustmomizeTargetAppManagerURLAction extends AbstractCloudDashModelAction{ + + protected CustmomizeTargetAppManagerURLAction(LiveExpression section, SimpleDIContext context) { + super(section, context); + setText("Customize Cloud Admin Console URL..."); + } + + @Override + public void updateEnablement() { + this.setEnabled(isApplicable(sectionSelection.getValue())); + } + + @Override + public void updateVisibility() { + this.setVisible(isApplicable(sectionSelection.getValue())); + } + + private boolean isApplicable(BootDashModel section) { + if (section != null && section instanceof CloudFoundryBootDashModel) { + PropertyStoreApi props = section.getRunTarget().getType().getPersistentProperties(); + return props != null; + } + return false; + } + + @Override + public void run() { + final BootDashModel section = sectionSelection.getValue(); + if (isApplicable(section)) { + CustomizeAppsManagerURLDialogModel model = new CustomizeAppsManagerURLDialogModel((CloudFoundryBootDashModel)section); + cfUi().openEditAppsManagerURLDialog(model); + } + } + + @Override + public EnumSet showIn() { + return EnumSet.of(Location.CUSTOMIZE_MENU); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/EnableJmxSshTunnelAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/EnableJmxSshTunnelAction.java new file mode 100644 index 000000000..424506789 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/EnableJmxSshTunnelAction.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2018 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.cf.actions; + +import java.net.URL; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.PlatformUI; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public class EnableJmxSshTunnelAction extends AbstractCloudAppDashElementsAction { + + private ElementStateListener stateListener; + + public EnableJmxSshTunnelAction(Params params) { + super(params); + this.setText("Enable JMX Ssh Tunneling"); + this.setToolTipText("Enables JMX on this app and creates an SSH Tunnel the next time the app is started."); + URL url = FileLocator.find(Platform.getBundle("org.springframework.ide.eclipse.boot.dash"), new Path("icons/link_to_editor.png"), null); + if (url != null) { + this.setImageDescriptor(ImageDescriptor.createFromURL(url)); + } + if (model != null) { + model.addElementStateListener(stateListener = new ElementStateListener() { + @Override + public void stateChanged(BootDashElement e) { + if (getSelectedElements().contains(e) && !PlatformUI.getWorkbench().isClosing()) { + PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + updateEnablement(); + } + }); + } + } + }); + } + } + + @Override + public void updateEnablement() { + boolean enable = false; + boolean enableTunnel = false; + if (getSelectedElements().size()==1) { + BootDashElement e = getSelectedElements().iterator().next(); + if (e instanceof CloudAppDashElement && e.getProject() != null) { + CloudAppDashElement cde = (CloudAppDashElement) e; + enable = true; + enableTunnel = !cde.getEnableJmxSshTunnel(); + } + } + this.setEnabled(enable); + if (enable) { + setText((enableTunnel ? "Enable" : "Disable")+" JMX Ssh Tunnelling"); + } else { + setText("Toggle JMX Ssh Tunnelling"); + } + } + + @Override + public void run() { + try { + for (BootDashElement _e : getSelectedElements()) { + if (_e instanceof CloudAppDashElement && _e.getBootDashModel() instanceof CloudFoundryBootDashModel && _e.getProject() != null) { + CloudAppDashElement e = (CloudAppDashElement) _e; + boolean enable = !e.getEnableJmxSshTunnel(); + e.setEnableJmxSshTunnel(enable); + RunState runstate = e.getRunState(); + if (runstate!=RunState.INACTIVE) { + int answer = ui().confirmOperation((enable ? "Enabling" : "Disabling") +" JMX Requires Restart", + "Enabling JMX is done by setting environment parameters on startup. For this setting to take effect the app needs to be restarted.\n\n" + + "Do you want to restart now?", + new String[] { + "Yes, Restart NOW", + "No, I'll restart MANUALLY later" + }, + 0 + ); + if (answer==0) { + RunState runningOrDebugging = runstate==RunState.DEBUGGING ? RunState.DEBUGGING : RunState.RUNNING; + e.restart(runningOrDebugging, ui()); + } + } + } + } + } catch (Exception e) { + Log.log(e); + } + } + + @Override + public void dispose() { + if (model != null && stateListener != null) { + model.removeElementStateListener(stateListener); + stateListener = null; + } + super.dispose(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/OpenCloudAdminConsoleAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/OpenCloudAdminConsoleAction.java new file mode 100644 index 000000000..9eff60721 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/OpenCloudAdminConsoleAction.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.actions; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +/** + * @author Martin Lippert + */ +public class OpenCloudAdminConsoleAction extends AbstractCloudDashModelAction { + + public OpenCloudAdminConsoleAction(LiveExpression sectionSelection, SimpleDIContext ui) { + super(sectionSelection, ui); + this.setText("Open Cloud Admin Console"); + this.setToolTipText("Opens the Cloud Administration Console for this Cloud Foundry Space and Organisation"); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/cloud_obj.png")); + } + + @Override + public void run() { + final BootDashModel targetModel = sectionSelection.getValue(); + if (targetModel != null) { + RunTarget target = targetModel.getRunTarget(); + if (target instanceof CloudFoundryRunTarget) { + ((CloudFoundryRunTarget) target).openCloudAdminConsole(ui()); + } + } + } + + @Override + public void updateEnablement() { + final BootDashModel targetModel = sectionSelection.getValue(); + if (targetModel != null) { + RunTarget runTarget = targetModel.getRunTarget(); + this.setEnabled(runTarget instanceof CloudFoundryRunTarget); + } else { + this.setEnabled(false); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/ReconnectCloudConsoleAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/ReconnectCloudConsoleAction.java new file mode 100644 index 000000000..c84af7442 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/ReconnectCloudConsoleAction.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.actions; + +import java.util.Collection; +import java.util.Iterator; + +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.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction.Params; + +public class ReconnectCloudConsoleAction extends AbstractCloudAppDashElementsAction { + + public ReconnectCloudConsoleAction(Params params) { + super(params); + this.setText("Reconnect Console"); + this.setToolTipText("Reconnect Console"); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/open_console.png")); + this.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/open_console_disabled.png")); + } + + @Override + public void run() { + final Collection selecteds = getSelectedElements(); + + Job job = new Job("Reconnecting console") { + + @Override + protected IStatus run(IProgressMonitor monitor) { + doShowConsoles(selecteds); + return Status.OK_STATUS; + } + + }; + job.schedule(); + } + + protected void doShowConsoles(Collection selectedElements) { + + if (selectedElements != null) { + + Iterator it = selectedElements.iterator(); + + // Show first element only for now + if (it.hasNext()) { + BootDashElement element = selectedElements.iterator().next(); + BootDashModel model = element.getBootDashModel(); + try { + if (model.getElementConsoleManager() != null) { + model.getElementConsoleManager().reconnect(element); + } + } catch (Exception e) { + ui().errorPopup("Reconnect Console Failure", e.getMessage()); + } + } + } + } + + @Override + public void updateEnablement() { + boolean enable = false; + if (!getSelectedElements().isEmpty()) { + enable = true; + for (BootDashElement e : getSelectedElements()) { + if (!(e instanceof CloudAppDashElement)) { + enable = false; + break; + } + } + } + + this.setEnabled(enable); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/RestartApplicationOnlyAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/RestartApplicationOnlyAction.java new file mode 100644 index 000000000..184fcca11 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/RestartApplicationOnlyAction.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.actions; + +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.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction.Params; + +public class RestartApplicationOnlyAction extends AbstractCloudAppDashElementsAction { + + public RestartApplicationOnlyAction(Params params) { + super(params); + this.setText("Restart Only"); + this.setToolTipText("Restarts the application without uploading changes."); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/restart.png")); + this.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/restart_disabled.png")); + } + + @Override + public void run() { + Job job = new Job("Restarting apps") { + @Override + protected IStatus run(IProgressMonitor monitor) { + for (BootDashElement el : getSelectedElements()) { + + if (el instanceof CloudAppDashElement) { + try { + CloudAppDashElement cloudAppDashElement = (CloudAppDashElement) el; + cloudAppDashElement.restartOnlyAsynch(ui(), cloudAppDashElement.createCancelationToken()); + } catch (Exception e) { + ui().errorPopup("Error restarting application", e.getMessage()); + } + } + } + return Status.OK_STATUS; + } + + }; + job.schedule(); + } + + @Override + public void updateEnablement() { + boolean enable = false; + if (!getSelectedElements().isEmpty()) { + enable = true; + for (BootDashElement e : getSelectedElements()) { + if (!(e instanceof CloudAppDashElement)) { + enable = false; + break; + } + } + } + + this.setEnabled(enable); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/SelectManifestAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/SelectManifestAction.java new file mode 100644 index 000000000..fab71e1aa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/SelectManifestAction.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.actions; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.ops.SelectManifestOp; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +public class SelectManifestAction extends AbstractCloudAppDashElementsAction { + + public SelectManifestAction(Params params) { + super(params); + params.getContext().assertDefinitionFor(UserInteractions.class); + this.setText("Select Manifest"); + this.setToolTipText("Selects a manifest YAML file to use during application restart."); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/selectmanifest.gif")); + } + + @Override + public void run() { + CloudAppDashElement element = getSelectedCloudElementWithProject(); + if (element != null) { + CloudFoundryBootDashModel model = element.getBootDashModel(); + model.runAsynch(new SelectManifestOp(element), ui()); + } + } + + @Override + public void updateEnablement() { + this.setEnabled(getSelectedCloudElementWithProject() != null); + } + + protected CloudAppDashElement getSelectedCloudElementWithProject() { + if (getSelectedElements().size() == 1) { + + for (BootDashElement e : getSelectedElements()) { + if (e instanceof CloudAppDashElement) { + CloudAppDashElement cde = (CloudAppDashElement) e; + if (cde.getProject() != null && cde.getProject().isAccessible()) { + return cde; + } + } + } + } + return null; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/UpdatePasswordAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/UpdatePasswordAction.java new file mode 100644 index 000000000..d7d215c9d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/actions/UpdatePasswordAction.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.actions; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.eclipse.core.runtime.Status; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.RunTargetWithProperties; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public class UpdatePasswordAction extends AbstractCloudDashModelAction { + + public UpdatePasswordAction(LiveExpression sectionSelection, SimpleDIContext context) { + super(sectionSelection, context); + this.setText("Update Password"); + this.setToolTipText("Update password locally for the selected target."); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/update_password.png")); + } + + CompletableFuture lastRunCompletion = null; + + /** + * This is meant for test-code only, so that test can we wait for the action (part f which is executed in + * a job) to complete. + */ + public void waitFor() throws InterruptedException, ExecutionException { + if (lastRunCompletion!=null) { + lastRunCompletion.get(); + } + } + + @Override + public void run() { + CompletableFuture lastRunCompletion = this.lastRunCompletion = new CompletableFuture<>(); + try { + final CloudFoundryBootDashModel targetModel = (CloudFoundryBootDashModel) sectionSelection.getValue(); + final CloudFoundryRunTarget runTarget = targetModel.getRunTarget(); + if (runTarget!=null) { + targetModel.refreshTracker.callAsync("Updating password...", () -> { + runTarget.updatePasswordAndConnect(); + lastRunCompletion.complete(null); + return Status.OK_STATUS; + }); + } + } catch (Exception e) { + Log.log(e); + } + } + + @Override + public void updateEnablement() { + this.setEnabled(getCredentialsHolder(sectionSelection.getValue())!=null); + } + + @Override + public void updateVisibility() { + setVisible(getCredentialsHolder(sectionSelection.getValue()) != null); + } + + private RunTargetWithProperties getCredentialsHolder(BootDashModel section) { + if (section!=null) { + RunTarget target = section.getRunTarget(); + if (target instanceof RunTargetWithProperties) { + RunTargetWithProperties targetWithProps = (RunTargetWithProperties) target; + if (targetWithProps.requiresCredentials()) { + return targetWithProps; + } + } + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFAppState.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFAppState.java new file mode 100644 index 000000000..5b8ead367 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFAppState.java @@ -0,0 +1,9 @@ +package org.springframework.ide.eclipse.boot.dash.cf.client; + +public enum CFAppState { + + STOPPED, + STARTED, + UNKNOWN + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFApplication.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFApplication.java new file mode 100644 index 000000000..341ea5819 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFApplication.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public interface CFApplication extends CFEntity { + //TODO: lots of this infos should be moved to application details + + int getInstances(); + int getRunningInstances(); + int getMemory(); + UUID getGuid(); + List getServices(); + String getBuildpackUrl(); + List getUris(); + CFAppState getState(); + int getDiskQuota(); + Integer getTimeout(); + String getCommand(); + String getStack(); + + Map getEnvAsMap(); + String getHealthCheckType(); + String getHealthCheckHttpEndpoint(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFApplicationArchive.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFApplicationArchive.java new file mode 100644 index 000000000..e91ecf189 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFApplicationArchive.java @@ -0,0 +1,5 @@ +package org.springframework.ide.eclipse.boot.dash.cf.client; + +public interface CFApplicationArchive { + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFApplicationDetail.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFApplicationDetail.java new file mode 100644 index 000000000..591dfb285 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFApplicationDetail.java @@ -0,0 +1,17 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +import java.util.List; + +public interface CFApplicationDetail extends CFApplication { + List getInstanceDetails(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFBuildpack.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFBuildpack.java new file mode 100644 index 000000000..40f2704ef --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFBuildpack.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +public interface CFBuildpack { + String getName(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFClientParams.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFClientParams.java new file mode 100644 index 000000000..3632d0bef --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFClientParams.java @@ -0,0 +1,168 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +import java.net.URI; + +import org.eclipse.core.runtime.Assert; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials.CFCredentialType; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryTargetProperties; + +/** + * All the parameters needed to create a CF client. + * + * @author Kris De Volder + */ +public class CFClientParams { + + private final String apiUrl; + private final String username; + private CFCredentials credentials; + private final boolean isSelfSigned; + private final boolean skipSslValidation; + + private String orgName; // optional + private String spaceName; //optional + + public CFClientParams(String apiUrl, + String username, + CFCredentials credentials, + boolean isSelfSigned, + String orgName, + String spaceName, + boolean skipSslValidation + ) { + Assert.isNotNull(apiUrl, "apiUrl is required"); + Assert.isNotNull(credentials, "credentials required"); + if (credentials.getType()==CFCredentialType.PASSWORD) { + Assert.isNotNull(username, "username is required"); + } + this.apiUrl = apiUrl; + this.username = username; + this.credentials = credentials; + this.isSelfSigned = isSelfSigned; + this.skipSslValidation = skipSslValidation; + this.orgName = orgName; + this.spaceName = spaceName; + } + + public CFClientParams(CloudFoundryTargetProperties targetProperties) throws Exception { + this( + targetProperties.getUrl(), + targetProperties.getUsername(), + targetProperties.getCredentials(), + targetProperties.isSelfsigned(), + targetProperties.getOrganizationName(), + targetProperties.getSpaceName(), + targetProperties.skipSslValidation() + ); + } + + public CFCredentials getCredentials() { + return credentials; + } + + public String getUsername() { + return username; + } + + public boolean isSelfsigned() { + return isSelfSigned; + } + + public boolean skipSslValidation() { + return skipSslValidation; + } + + public String getApiUrl() { + return apiUrl; + } + + public String getOrgName() { + return orgName; + } + + public void setOrgName(String orgName) { + this.orgName = orgName; + } + + public String getSpaceName() { + return spaceName; + } + + public void setSpaceName(String spaceName) { + this.spaceName = spaceName; + } + + public String getHost() { + try { + URI uri = new URI(getApiUrl()); + return uri.getHost(); + } catch (Exception e) { + BootActivator.log(e); + } + return null; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((apiUrl == null) ? 0 : apiUrl.hashCode()); + result = prime * result + (isSelfSigned ? 1231 : 1237); + result = prime * result + ((orgName == null) ? 0 : orgName.hashCode()); + result = prime * result + ((credentials == null) ? 0 : credentials.hashCode()); + result = prime * result + ((spaceName == null) ? 0 : spaceName.hashCode()); + result = prime * result + ((username == null) ? 0 : username.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CFClientParams other = (CFClientParams) obj; + if (apiUrl == null) { + if (other.apiUrl != null) + return false; + } else if (!apiUrl.equals(other.apiUrl)) + return false; + if (isSelfSigned != other.isSelfSigned) + return false; + if (orgName == null) { + if (other.orgName != null) + return false; + } else if (!orgName.equals(other.orgName)) + return false; + if (credentials == null) { + if (other.credentials != null) + return false; + } else if (!credentials.equals(other.credentials)) + return false; + if (spaceName == null) { + if (other.spaceName != null) + return false; + } else if (!spaceName.equals(other.spaceName)) + return false; + if (username == null) { + if (other.username != null) + return false; + } else if (!username.equals(other.username)) + return false; + return true; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFCloudDomain.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFCloudDomain.java new file mode 100644 index 000000000..041f81e7b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFCloudDomain.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFDomainStatus; + +public interface CFCloudDomain { + String getName(); + CFDomainType getType(); + CFDomainStatus getStatus(); + + /** + * If the given hostAndDomain is of the form ${host}.${domain} then + * return the host part. Otherwise return null. + */ + default String splitHost(String hostAndDomain) { + String name = getName(); + if (hostAndDomain.endsWith("."+name)) { + return hostAndDomain.substring(0, hostAndDomain.length()-name.length()-1); + } + return null; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFCredentials.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFCredentials.java new file mode 100644 index 000000000..26548feee --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFCredentials.java @@ -0,0 +1,151 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.core.runtime.Assert; +import org.springsource.ide.eclipse.commons.livexp.ui.Ilabelable; + +public class CFCredentials { + + public enum CFCredentialType { + PASSWORD, + TEMPORARY_CODE, + REFRESH_TOKEN; + + public LoginMethod toLoginMethod() { + switch (this) { + case PASSWORD: + return LoginMethod.PASSWORD; + case TEMPORARY_CODE: + return LoginMethod.TEMPORARY_CODE; + default: + return null; + } + } + } + + public enum LoginMethod implements Ilabelable { + PASSWORD, + TEMPORARY_CODE; + + @Override + public String getLabel() { + String[] pieces = name().split("_"); + StringBuilder label = new StringBuilder(); + for (int i = 0; i < pieces.length; i++) { + if (i>0) { + label.append(" "); + } + label.append(StringUtils.capitalize(pieces[i].toLowerCase())); + } + return label.toString(); + } + } + + private final CFCredentialType type; + private final String secret; + + /** + * Deprecated, use fromLogin instead + */ + @Deprecated + public static CFCredentials fromPassword(String password) { + return fromLogin(LoginMethod.PASSWORD, password); + } + + + public static CFCredentials fromLogin(LoginMethod method, String secret) { + CFCredentialType type; + switch (method) { + case PASSWORD: + type = CFCredentialType.PASSWORD; + break; + case TEMPORARY_CODE: + type = CFCredentialType.TEMPORARY_CODE; + break; + default: + throw new IllegalStateException("Bug! Missing switch case?"); + } + return new CFCredentials(type, secret); + } + + public static CFCredentials fromRefreshToken(String refreshToken) { + Assert.isNotNull(refreshToken); + return new CFCredentials(CFCredentialType.REFRESH_TOKEN, refreshToken); + } + + public String getSecret() { + return secret; + } + + ///////////////////////////////////////////////////////////////////////// + + + /** + * Private constuctor, use static `fromXXX` factory methods instead. + */ + private CFCredentials(CFCredentialType type, String secret) { + this.type = type; + this.secret = secret; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((secret == null) ? 0 : secret.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CFCredentials other = (CFCredentials) obj; + if (secret == null) { + if (other.secret != null) + return false; + } else if (!secret.equals(other.secret)) + return false; + if (type != other.type) + return false; + return true; + } + + @Override + public String toString() { + return "CFCredentials [type=" + type + ", secret=" + hidePassword(type, secret) + "]"; + } + + private String hidePassword(CFCredentialType type, String password) { + if (password==null) { + return null; + } + return type==CFCredentialType.PASSWORD + ? "****" + : password; + } + + + public CFCredentialType getType() { + return type; + } + + public static CFCredentials fromSsoToken(String ssoToken) { + return CFCredentials.fromLogin(LoginMethod.TEMPORARY_CODE, ssoToken); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFDomainType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFDomainType.java new file mode 100644 index 000000000..c3045c34a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFDomainType.java @@ -0,0 +1,16 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.client; + +public enum CFDomainType { + HTTP, TCP +} + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFEntity.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFEntity.java new file mode 100644 index 000000000..ff7618bff --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFEntity.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +public interface CFEntity { + String getName(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFExceptions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFExceptions.java new file mode 100644 index 000000000..1ababe0a5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFExceptions.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +import org.cloudfoundry.uaa.UaaException; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +/** + * Static methods to recognize specific types of exceptions CF client + * may throw. + * + * @author Kris De Volder + */ +public class CFExceptions { + + public static boolean isAuthFailure(Exception e) { + String msg = ExceptionUtil.getMessage(e); + return msg.contains("Bad credentials") || ExceptionUtil.getDeepestCause(e) instanceof UaaException; + } + + public static boolean isSSLCertificateFailure(Exception e) { + Throwable cause = ExceptionUtil.getDeepestCause(e); + return cause.getClass().getName().equals("sun.security.provider.certpath.SunCertPathBuilderException"); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFInstanceState.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFInstanceState.java new file mode 100644 index 000000000..cc98f66b6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFInstanceState.java @@ -0,0 +1,5 @@ +package org.springframework.ide.eclipse.boot.dash.cf.client; + +public enum CFInstanceState { + RUNNING, CRASHED, FLAPPING, STARTING, DOWN, UNKNOWN +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFInstanceStats.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFInstanceStats.java new file mode 100644 index 000000000..6d0953f8f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFInstanceStats.java @@ -0,0 +1,7 @@ +package org.springframework.ide.eclipse.boot.dash.cf.client; + +public interface CFInstanceStats { + + CFInstanceState getState(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFOrganization.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFOrganization.java new file mode 100644 index 000000000..a6504fa02 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFOrganization.java @@ -0,0 +1,17 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +import java.util.UUID; + +public interface CFOrganization extends CFEntity { + UUID getGuid(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFServiceInstance.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFServiceInstance.java new file mode 100644 index 000000000..fa9f57d8f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFServiceInstance.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +public interface CFServiceInstance extends CFEntity { + + String getName(); + String getService(); + String getPlan(); + String getDescription(); + String getDocumentationUrl(); + String getDashboardUrl(); + + //TODO: last operation info? + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFSpace.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFSpace.java new file mode 100644 index 000000000..bb5e0b553 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFSpace.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +import java.util.UUID; + +public interface CFSpace extends CFEntity { + CFOrganization getOrganization(); + UUID getGuid(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFStack.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFStack.java new file mode 100644 index 000000000..29be393fa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CFStack.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +public interface CFStack extends CFEntity { + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/ClientRequests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/ClientRequests.java new file mode 100644 index 000000000..6389e341c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/ClientRequests.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.client; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.osgi.framework.Version; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFPushArguments; +import org.springframework.ide.eclipse.boot.dash.console.IApplicationLogConsole; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface ClientRequests extends Disposable { + + /** + * The actual Rest API version that cloud controller claims to be. + */ + Version getApiVersion() throws Exception; + + /** + * The minimum version that the CF V2 java client claims to support. + */ + Version getSupportedApiVersion(); + + /** + * Returns null if the application does not exist. Throws some kind of Exception if there's any other kind of problem. + */ + CFApplicationDetail getApplication(String appName) throws Exception; + + //TODO: consider removing the getXXXSupport method and directly adding the apis that these support + // objects provide. + SshClientSupport getSshClientSupport() throws Exception; + + + void deleteApplication(String name) throws Exception; + + List getApplicationsWithBasicInfo() throws Exception; + List getBuildpacks() throws Exception; + List getDomains() throws Exception; + List getServices() throws Exception; + List getSpaces() throws Exception; + List getStacks() throws Exception; + void restartApplication(String appName, CancelationToken token) throws Exception; + void stopApplication(String appName) throws Exception; + reactor.core.Disposable streamLogs(String appName, IApplicationLogConsole logConsole) throws Exception; + Flux getApplicationDetails(List appsToLookUp) throws Exception; + String getHealthCheck(UUID appGuid) throws Exception; + void setHealthCheck(UUID guid, String hcType) throws Exception; + boolean applicationExists(String appName) throws Exception; + + //Added since v2: + void push(CFPushArguments args, CancelationToken cancelationToken) throws Exception; + Map getApplicationEnvironment(String appName) throws Exception; + Mono deleteServiceAsync(String serviceName); + + /** + * Gets current value of the client's refresh token. Note that the token is only set once it is known. + * Initially, if a client is created via password auth, then the refreshToken won't be known until + * some operation has been executed. + * + * @return Refresh token if it is already known, null otherwise. + */ + String getRefreshToken(); + Mono getUserName(); + + /** + * The returned flux will provide the current token in its onNext immediately, if there is a current + * token already. Subsequent onNext will be fired whenever the token changes. + */ + Flux getRefreshTokens(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CloudAppInstances.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CloudAppInstances.java new file mode 100644 index 000000000..354b7167b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CloudAppInstances.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.client; + +import java.util.List; + +import org.eclipse.core.runtime.Assert; + +/** + * A Cloud application with additional stats and instances information + * + * This should be removed. Use {@link CFApplicationDetail} instead. It contains + * all the same infos. + */ +@Deprecated +public class CloudAppInstances { + + private final CFApplication app; + private final List stats; + + public CloudAppInstances(CFApplication app, List stats) { + Assert.isNotNull(app); + this.app = app; + this.stats = stats; + } + + public CFApplication getApplication() { + return app; + } + + public List getStats() { + return stats; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CloudFoundryClientFactory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CloudFoundryClientFactory.java new file mode 100644 index 000000000..0df2c19fc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/CloudFoundryClientFactory.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.client; + +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryTargetProperties; + +public abstract class CloudFoundryClientFactory { + + public abstract ClientRequests getClient(CFClientParams params); + + /** + * Get the client for an existing {@link CloudFoundryRunTarget}. Note that + * this may require the password to be set for that runtarget. + * + * @param runTarget + * @return client if connection was successful. + * @throws Exception + * if there was an error connecting, including if password is + * not set or invalid. + */ + public final ClientRequests getClient(CloudFoundryTargetProperties targetProperties) throws Exception { + return getClient(new CFClientParams(targetProperties)); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/HealthChecks.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/HealthChecks.java new file mode 100644 index 000000000..b531e8b10 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/HealthChecks.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * Copyright (c) 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.cf.client; + +public class HealthChecks { + + public static final String HC_PORT = "port"; + public static final String HC_PROCESS = "process"; + public static final String HC_HTTP = "http"; + public static final String[] HC_ALL = {HC_PORT, HC_PROCESS, HC_HTTP}; +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/InstanceState.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/InstanceState.java new file mode 100644 index 000000000..de67ab7c3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/InstanceState.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2009-2016 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.cf.client; + +/** + * Enum used for the state of an instance + * + * Note: copied over from CF V1 client code. + * + * @author Thomas Risberg + */ +public enum InstanceState { + DOWN, STARTING, RUNNING, CRASHED, FLAPPING, UNKNOWN; + + public static InstanceState valueOfWithDefault(String s) { + try { + return InstanceState.valueOf(s); + } catch (IllegalArgumentException e) { + return InstanceState.UNKNOWN; + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/RequestErrorHandler.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/RequestErrorHandler.java new file mode 100644 index 000000000..63c1f2c99 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/RequestErrorHandler.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client; + +public class RequestErrorHandler { + + /** + * + * @param e + * @return true if request error should be treated as an error and thrown. False error + * should be ignored. + */ + public boolean throwError(Throwable e) { + return true; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/SshClientSupport.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/SshClientSupport.java new file mode 100644 index 000000000..35cbef140 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/SshClientSupport.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.client; + +import java.util.UUID; + +public interface SshClientSupport { + + SshHost getSshHost() throws Exception; + String getSshUser(String appName, int instance) throws Exception; + String getSshCode() throws Exception; + + /** + * Deprecated because it is not supported with V2. Use the method based on appName instead. + */ + @Deprecated + String getSshUser(UUID appGuid, int instance) throws Exception; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/SshHost.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/SshHost.java new file mode 100644 index 000000000..f9fcf1975 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/SshHost.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.client; + +/** + * Info object containing various bits of info about the host to which an ssh + * client may wish to connect. + * + * @author Kris De Volder + */ +public class SshHost { + + final private String host; + + final private int port; + + final private String fingerPrint; + + public SshHost(String host, int port, String fingerPrint) { + super(); + this.host = host; + this.port = port; + this.fingerPrint = fingerPrint; + } + + public String getFingerPrint() { + return fingerPrint; + } + + public int getPort() { + return port; + } + + public String getHost() { + return host; + } + + @Override + public String toString() { + return "SshHost [host=" //$NON-NLS-1$ + + host + ", port=" //$NON-NLS-1$ + + port + ", fingerPrint=" //$NON-NLS-1$ + + fingerPrint + "]";//$NON-NLS-1$ + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/ApplicationExtras.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/ApplicationExtras.java new file mode 100644 index 000000000..aaf953e1a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/ApplicationExtras.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client.v2; + +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Mono; + +/** + * Extrat bits of info that are 'prefetched' asynchronously to fill + * out the CFApplication with info that C2 client doesn't initially return. + * + * @author Kris De Volder + */ +public interface ApplicationExtras { + Mono> getEnv(); + Mono> getServices(); + Mono getBuildpack(); + Mono getStack(); + Mono getTimeout(); + Mono getCommand(); + Mono getHealthCheckType(); + Mono getHealthCheckHttpEndpoint(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFApplicationDetailData.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFApplicationDetailData.java new file mode 100644 index 000000000..66800b1e4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFApplicationDetailData.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client.v2; + +import java.util.List; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplicationDetail; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFInstanceStats; + +public class CFApplicationDetailData extends CFApplicationSummaryData implements CFApplicationDetail { + + private List instanceDetails; + + public CFApplicationDetailData( + CFApplicationSummaryData app, + List instanceDetails + ) { + super( + app.getName(), + app.getInstances(), + app.getRunningInstances(), + app.getMemory(), + app.getGuid(), + app.getUris(), + app.getState(), + app.getDiskQuota(), + app.extras + ); + this.instanceDetails = instanceDetails; + } + + @Override + public List getInstanceDetails() { + return instanceDetails; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("CFApplicationDetail(\n"); + buf.append(" name = "+getName()+"\n"); + buf.append(" instance = "+getInstances()+"\n"); + buf.append(" runningInstances = "+getRunningInstances()+"\n"); + buf.append(" memory = "+getMemory()+"\n"); + buf.append(" guid = "+getGuid()+"\n"); + buf.append(" uris = "+getUris()+"\n"); + buf.append(" state = "+getState()+"\n"); + buf.append(" diskQuota = "+getDiskQuota()+"\n"); + buf.append(" instanceDetails = "+getInstanceDetails()+"\n"); + buf.append(")\n"); + return buf.toString(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFApplicationSummaryData.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFApplicationSummaryData.java new file mode 100644 index 000000000..4f7cd8dd2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFApplicationSummaryData.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client.v2; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFAppState; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; + +public class CFApplicationSummaryData implements CFApplication { + + private String name; + private int instances; + private int runningInstances; + private int memory; + private UUID guid; + private List uris; + private CFAppState state; + private int diskQuota; + protected ApplicationExtras extras; + + public CFApplicationSummaryData( + String name, + int instances, + int runningInstances, + int memory, + UUID guid, + List uris, + CFAppState state, + int diskQuota, + ApplicationExtras extras + ) { + super(); + this.name = name; + this.instances = instances; + this.runningInstances = runningInstances; + this.memory = memory; + this.guid = guid; + this.uris = uris; + this.state = state; + this.diskQuota = diskQuota; + this.extras = extras; + } + + @Override + public String getName() { + return name; + } + + @Override + public int getInstances() { + return instances; + } + + @Override + public int getRunningInstances() { + return runningInstances; + } + + @Override + public int getMemory() { + return memory; + } + + @Override + public UUID getGuid() { + return guid; + } + + @Override + public List getServices() { + return extras.getServices().block(DefaultClientRequestsV2.GET_SERVICES_TIMEOUT); + } + + @Override + public String getBuildpackUrl() { + return extras.getBuildpack().block(DefaultClientRequestsV2.GET_SMALL_INFO_TIMEOUT); + } + + @Override + public List getUris() { + return uris; + } + + @Override + public CFAppState getState() { + return state; + } + + @Override + public int getDiskQuota() { + return diskQuota; + } + + @Override + public Integer getTimeout() { + return extras.getTimeout().block(DefaultClientRequestsV2.GET_SMALL_INFO_TIMEOUT); + } + + @Override + public String getHealthCheckType() { + return extras.getHealthCheckType().block(DefaultClientRequestsV2.GET_SMALL_INFO_TIMEOUT); + } + + @Override + public String getCommand() { + return extras.getCommand().block(DefaultClientRequestsV2.GET_SMALL_INFO_TIMEOUT); + } + + @Override + public String getStack() { + return extras.getStack().block(DefaultClientRequestsV2.GET_SMALL_INFO_TIMEOUT); + } + + @Override + public Map getEnvAsMap() { + return extras.getEnv().block(DefaultClientRequestsV2.GET_SMALL_INFO_TIMEOUT); + } + + @Override + public String getHealthCheckHttpEndpoint() { + return extras.getHealthCheckHttpEndpoint().block(DefaultClientRequestsV2.GET_SMALL_INFO_TIMEOUT); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFCloudDomainData.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFCloudDomainData.java new file mode 100644 index 000000000..c8b4445b1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFCloudDomainData.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.client.v2; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFDomainType; + +public final class CFCloudDomainData implements CFCloudDomain { + private final String name; + private final CFDomainType type; + private final CFDomainStatus status; + + public CFCloudDomainData(String name, CFDomainType type, CFDomainStatus status) { + super(); + this.name = name; + this.type = type; + this.status = status; + } + + public CFCloudDomainData(String name) { + this(name, CFDomainType.HTTP, CFDomainStatus.SHARED); + } + + @Override + public CFDomainType getType() { + return type; + } + + @Override + public CFDomainStatus getStatus() { + return status; + } + + @Override + public String getName() { + return name; + } + + @Override + public String toString() { + return "CFCloudDomainData [name=" + name + ", type=" + type + ", status=" + status + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((status == null) ? 0 : status.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CFCloudDomainData other = (CFCloudDomainData) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (status != other.status) + return false; + if (type != other.type) + return false; + return true; + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFDomainStatus.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFDomainStatus.java new file mode 100644 index 000000000..87cd867f0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFDomainStatus.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.client.v2; + +public enum CFDomainStatus { + OWNED, SHARED +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFPushArguments.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFPushArguments.java new file mode 100644 index 000000000..9e53e63e6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFPushArguments.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client.v2; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipFile; + +import org.eclipse.core.runtime.Assert; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeploymentProperties; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +/** + * Arguments passed to push operation. + * + * @author Kris De Volder + * @author Nieraj Singh + */ +public class CFPushArguments implements AutoCloseable { + private List routes = ImmutableList.of(); + private String appName; + private Integer memory; + private Integer diskQuota; + private Integer timeout; + private String buildpack; + private String command; + private String stack; + private String healthCheckType; + private Map env = ImmutableMap.of(); + private Integer instances; + private List services = ImmutableList.of(); + private File applicationDataAsFile; + private boolean noStart = false; + private boolean randomRoute = false; + private String healthCheckHttpEndpoint; + + public CFPushArguments() { + } + + public String getAppName() { + return appName; + } + public void setAppName(String appName) { + this.appName = appName; + } + public Integer getMemory() { + return memory; + } + public void setMemory(Integer memory) { + this.memory = memory; + } + public Integer getDiskQuota() { + return diskQuota; + } + public void setDiskQuota(Integer diskQuota) { + this.diskQuota = diskQuota; + } + public Integer getTimeout() { + return timeout; + } + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + public String getBuildpack() { + return buildpack; + } + public void setBuildpack(String buildpack) { + this.buildpack = buildpack; + } + public String getCommand() { + return command; + } + public void setCommand(String command) { + this.command = command; + } + public String getStack() { + return stack; + } + public void setStack(String stack) { + this.stack = stack; + } + public Map getEnv() { + return env; + } + public void setEnv(Map env) { + this.env = env; + } + public Integer getInstances() { + return instances; + } + public void setInstances(Integer instances) { + this.instances = instances; + } + public List getServices() { + return services; + } + public void setServices(List services) { + this.services = services; + } + public boolean getRandomRoute() { + return this.randomRoute; + } + public void setRandomRoute(boolean randomRoute) { + this.randomRoute = randomRoute; + } + public File getApplicationDataAsFile() { + return applicationDataAsFile; + } + public void setApplicationData(File archive) throws Exception { + Assert.isLegal(this.applicationDataAsFile==null, "Can only set this once"); + this.applicationDataAsFile=archive; + } + public boolean isNoStart() { + return noStart; + } + public void setNoStart(boolean noStart) { + this.noStart = noStart; + } + public String getHealthCheckType() { + if (healthCheckType==null) { + return DeploymentProperties.DEFAULT_HEALTH_CHECK_TYPE; + } + return healthCheckType; + } + public void setHealthCheckType(String healthCheckType) { + this.healthCheckType = healthCheckType; + } + public List getRoutes() { + return routes; + } + public void setRoutes(Collection routes) { + this.routes = routes == null ? ImmutableList.of() : ImmutableList.copyOf(routes); + } + public void setRoutes(String... routes) { + setRoutes(ImmutableList.copyOf(routes)); + } + @Override + public String toString() { + return "CFPushArguments [appName=" + appName + ", routes=" + routes + ", memory=" + memory + ", diskQuota=" + + diskQuota + ", timeout=" + timeout + ", buildpack=" + buildpack + ", command=" + command + ", stack=" + + stack + ", env=" + env + ", instances=" + instances + ", services=" + services + ", noStart=" + + noStart + ", healthCheckType="+ healthCheckType+ ", randomRoute="+ randomRoute+" ]"; + } + + public void setHealthCheckHttpEndpoint(String healthCheckHttpEndpoint) { + this.healthCheckHttpEndpoint = healthCheckHttpEndpoint; + } + + public String getHealthCheckHttpEndpoint() { + return this.healthCheckHttpEndpoint; + } + + @Override + public void close() throws Exception { + //This used to do something more useful, we kept it for now to avoid having to + // change a lot of code. But calling `close` is no longer needed and does nothing. + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFRoute.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFRoute.java new file mode 100644 index 000000000..5a1a6e686 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFRoute.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * Copyright (c) 2016, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.client.v2; + +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +public class CFRoute { + + public static final int NO_PORT = -1; + public static final String EMPTY_ROUTE = ""; + + final private String domain; + final private String host; + final private String path; + final private int port; + final private String fullRoute; + + CFRoute(String domain, String host, String path, int port, String fullRoute) { + super(); + this.domain = domain; + this.host = host; + this.path = path; + this.port = port; + this.fullRoute = fullRoute; + } + + public String getDomain() { + return domain; + } + + public String getHost() { + return host; + } + + public String getPath() { + return path; + } + + public int getPort() { + return port; + } + + public String getRoute() { + return fullRoute; + } + + public void validate() throws Exception { + if (!StringUtil.hasText(getDomain())) { + throw ExceptionUtil.coreException("Invalid route: No domain set."); + } + } + + @Override + public String toString() { + return "CFRoute [domain=" + domain + ", host=" + host + ", path=" + path + ", port=" + port +"]"; + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((domain == null) ? 0 : domain.hashCode()); + result = prime * result + ((fullRoute == null) ? 0 : fullRoute.hashCode()); + result = prime * result + ((host == null) ? 0 : host.hashCode()); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + result = prime * result + port; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CFRoute other = (CFRoute) obj; + if (domain == null) { + if (other.domain != null) + return false; + } else if (!domain.equals(other.domain)) + return false; + if (fullRoute == null) { + if (other.fullRoute != null) + return false; + } else if (!fullRoute.equals(other.fullRoute)) + return false; + if (host == null) { + if (other.host != null) + return false; + } else if (!host.equals(other.host)) + return false; + if (path == null) { + if (other.path != null) + return false; + } else if (!path.equals(other.path)) + return false; + if (port != other.port) + return false; + return true; + } + + public static CFRouteBuilder builder() { + return new CFRouteBuilder(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFRouteBuilder.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFRouteBuilder.java new file mode 100644 index 000000000..1c081d19d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFRouteBuilder.java @@ -0,0 +1,248 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.client.v2; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.cloudfoundry.operations.routes.Route; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; + +public class CFRouteBuilder { + private String domain; + private String host; + private String path; + private int port = CFRoute.NO_PORT; + private String fullRoute; + + public CFRoute build() { + return new CFRoute(this.domain, this.host, this.path, this.port, this.fullRoute); + } + + public CFRouteBuilder domain(String domain) { + this.domain = domain; + // may seem like the more ideal place is to build the full route when + // building the route, rather than repeating + // the process each time a domain, host, path or port value is set + // but the "from" option should be allowed to overwrite the full route + // as well since it already + // has the full value. Therefore re-construct the full value if the + // route is being built piece by piece, but not in from + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder host(String host) { + this.host = host; + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder path(String path) { + this.path = path; + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder port(int port) { + this.port = port; + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder from(Route route) { + // Route doesn't seem to have API to get a port + this.port = CFRoute.NO_PORT; + this.domain = route.getDomain(); + this.host = route.getHost(); + this.path = route.getPath(); + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + /** + * Builds a {@link CFRoute} given a desiredUrl. This does NOT validate, and + * will attempt to build a route the best way it can given the desiredUrl. + * External components, like the CF Java client, can then validate the + * CFRoute. + * + * @param desiredUrl + * @param domains + * @return this builder + */ + public CFRouteBuilder from(String desiredUrl, Collection domains) { + + + //If it is empty or null, there is nothing to build. However, be sure that the + // full route value is non-null, even if the "components" may be null + if (!StringUtil.hasText(desiredUrl)) { + this.fullRoute = CFRoute.EMPTY_ROUTE; + return this; + } else { + // Be sure to set the full route. + this.fullRoute = desiredUrl; + } + + // Based on CLI cf/actors/routes.go and testing CLI directly with + // different "routes" values: + // 1. Paths is not allowed in TCP route (valid TCP route: + // "tcp.spring.io:8888") + // 2. Ports are not allowed in HTTP route (valid HTTP route: + // "myapps.cfapps.io/pathToApp/home") + // 3. Schemes (e.g. "http://") are not allowed in routes values. + // Anything that has a ":" is assumed to be TCP route followed by a port + // 4. Route can just be domain, or host and domain + // + // Therefore, routes values cannot be treated as URIs or URLs, but a + // combination of domain, host, path and port + // NOTE: The validation above doesn't need to take place here. The + // client or CF will validate correct combinations of routes. + // However, We may want to implement similar + // validation to the CF manifest editor though. + + String matchedHost = null; + String hostAndDomain = null; + + // Split into hostDomain segment, port and path + int slashIndex = desiredUrl.indexOf('/'); + if (slashIndex >= 0) { + hostAndDomain = desiredUrl.substring(0, slashIndex); + String tempPath = desiredUrl.substring(slashIndex); + // Do not set empty strings. If there is no path, then it should be + // null + if (StringUtil.hasText(tempPath)) { + this.path = tempPath; + } + } else { + hostAndDomain = desiredUrl; + } + + // CF Route builder does not validate, so don't allow exceptions to + // prevent parsing of the route. The builder should attempt to build + // a route the best way it can, even if it may have invalid information. + // This allows external participants, like the CF Java client, to + // perform validation + try { + String[] portSegments = hostAndDomain.split(":"); + if (portSegments.length == 2) { + hostAndDomain = portSegments[0]; + this.port = Integer.parseInt(portSegments[1]); + } + } catch (NumberFormatException e) { + Log.log(e); + } + + this.domain = findDomain(hostAndDomain, domains); + + if (this.domain != null) { + matchedHost = hostAndDomain.substring(0, hostAndDomain.length() - this.domain.length()); + if (matchedHost.endsWith(".")) { + matchedHost = matchedHost.substring(0, matchedHost.length() - 1); + } + + // Don't set empty strings + if (StringUtil.hasText(matchedHost)) { + this.host = matchedHost; + } + } else { + // Do a basic split on '.', where first segment is the host, and the + // rest domain + int firstDotIndex = hostAndDomain.indexOf('.'); + if (firstDotIndex >= 0) { + String tempDomain = hostAndDomain.substring(firstDotIndex + 1); + // Don't set empty strings + if (StringUtil.hasText(tempDomain)) { + this.domain = tempDomain; + } + + String tempHost = hostAndDomain.substring(0, firstDotIndex); + if (StringUtil.hasText(tempHost)) { + this.host = tempHost; + } + } else { + if (StringUtil.hasText(hostAndDomain)) { + this.host = hostAndDomain; + } + } + } + + return this; + } + + public static String findDomain(String hostDomain, Collection domains) { + if (hostDomain == null) { + return null; + } + // find exact match + for (String name : domains) { + if (hostDomain.equals(name)) { + return hostDomain; + } + } + // Otherwise split on the first "." and try again + if (hostDomain.indexOf(".") >= 0 && hostDomain.indexOf(".") + 1 < hostDomain.length()) { + String remaining = hostDomain.substring(hostDomain.indexOf(".") + 1, hostDomain.length()); + return findDomain(remaining, domains); + } else { + return null; + } + } + + /** + * A basic building of a full route value. It performs no validation, just + * builds based on whether the parameter are set + * + * @param host + * @param domain + * @param path + * @param port + * @return Route value build with the given components. Always returns a non-null route. Empty route if no arguments are passed. + */ + public static String buildRouteVal(String host, String domain, String path, int port) { + + StringBuilder builder = new StringBuilder(); + if (StringUtil.hasText(host)) { + builder.append(host); + } + + if (StringUtil.hasText(domain)) { + if (StringUtil.hasText(host)) { + builder.append('.'); + } + builder.append(domain); + } + + if (port != CFRoute.NO_PORT) { + builder.append(':'); + builder.append(Integer.toString(port)); + } + + if (StringUtil.hasText(path)) { + if (!path.startsWith("/")) { + builder.append('/'); + } + builder.append(path); + } + + return builder.toString(); + } + + public CFRouteBuilder from(String desiredUrl, List cloudDomains) { + List domains = cloudDomains + .stream() + .map(CFCloudDomain::getName) + .collect(Collectors.toList()); + return from(desiredUrl, domains); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFWrappingV2.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFWrappingV2.java new file mode 100644 index 000000000..0e0c7a00c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CFWrappingV2.java @@ -0,0 +1,293 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client.v2; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.cloudfoundry.client.v2.buildpacks.BuildpackResource; +import org.cloudfoundry.operations.applications.ApplicationDetail; +import org.cloudfoundry.operations.applications.ApplicationSummary; +import org.cloudfoundry.operations.applications.InstanceDetail; +import org.cloudfoundry.operations.domains.Domain; +import org.cloudfoundry.operations.domains.Status; +import org.cloudfoundry.operations.organizations.OrganizationSummary; +import org.cloudfoundry.operations.services.ServiceInstance; +import org.cloudfoundry.operations.services.ServiceInstanceType; +import org.cloudfoundry.operations.spaces.SpaceSummary; +import org.cloudfoundry.operations.stacks.Stack; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFAppState; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplicationDetail; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFBuildpack; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFDomainType; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFInstanceState; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFInstanceStats; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFOrganization; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFServiceInstance; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFSpace; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFStack; +import org.springframework.ide.eclipse.boot.util.Log; + +import com.google.common.collect.ImmutableList; + +/** + * Various helper methods to 'wrap' objects returned by CF client into + * our own types, so that we do not directly expose library types to our + * code. + * + * @author Kris De Volder + */ +public class CFWrappingV2 { + + public static CFBuildpack wrap(BuildpackResource rsrc) { + String name = rsrc.getEntity().getName(); + return new CFBuildpack() { + @Override + public String getName() { + return name; + } + }; + } + + public static CFApplicationDetail wrap(ApplicationDetail details, ApplicationExtras extras) { + if (details!=null) { + List instances = ImmutableList.copyOf( + details.getInstanceDetails() + .stream() + .map(CFWrappingV2::wrap) + .collect(Collectors.toList()) + ); + CFApplicationSummaryData summary = wrapSummary(details, extras); + return new CFApplicationDetailData( + summary, + instances + ); + } + return null; + } + + public static CFStack wrap(Stack stack) { + if (stack!=null) { + String name = stack.getName(); + return new CFStack() { + @Override + public String getName() { + return name; + } + + @Override + public String toString() { + return "CFStack("+name+")"; + } + }; + } + return null; + } + + public static CFApplicationDetail wrap( + CFApplicationSummaryData summary, + ApplicationDetail details + ) { + List instanceDetails = ImmutableList.copyOf( + details + .getInstanceDetails() + .stream() + .map(CFWrappingV2::wrap) + .collect(Collectors.toList()) + ); + return new CFApplicationDetailData(summary, instanceDetails); + } + + public static CFCloudDomain wrap(Domain domain) { + if (domain!=null) { + return new CFCloudDomainData( + domain.getName(), + CFWrappingV2.wrapDomainType(domain.getType()), + CFWrappingV2.wrap(domain.getStatus()) + ); + } + return null; + } + + public static CFDomainStatus wrap(Status status) { + if (status!=null) { + return CFDomainStatus.valueOf(status.name()); + } + return null; + } + + public static CFDomainType wrapDomainType(String type) { + if (type!=null) { + return CFDomainType.valueOf(type.toUpperCase()); + } + return CFDomainType.HTTP; + } + + public static CFInstanceStats wrap(InstanceDetail instanceDetail) { + return new CFInstanceStats() { + @Override + public CFInstanceState getState() { + try { + return CFInstanceState.valueOf(instanceDetail.getState()); + } catch (Exception e) { + Log.log(e); + return CFInstanceState.UNKNOWN; + } + } + + @Override + public String toString() { + return ""+getState(); + } + }; + } + + private static CFApplicationSummaryData wrapSummary(ApplicationDetail app, ApplicationExtras extras) { + CFAppState state; + try { + state = CFAppState.valueOf(app.getRequestedState()); + } catch (Exception e) { + Log.log(e); + state = CFAppState.UNKNOWN; + } + + return new CFApplicationSummaryData( + app.getName(), + app.getInstances(), + app.getRunningInstances(), + app.getMemoryLimit(), + UUID.fromString(app.getId()), + app.getUrls(), + state, + app.getDiskQuota(), + extras + ); + } + + public static CFApplication wrap(ApplicationSummary app, ApplicationExtras extras) { + CFAppState state; + try { + state = CFAppState.valueOf(app.getRequestedState()); + } catch (Exception e) { + Log.log(e); + state = CFAppState.UNKNOWN; + } + + return new CFApplicationSummaryData( + app.getName(), + app.getInstances(), + app.getRunningInstances(), + app.getMemoryLimit(), + UUID.fromString(app.getId()), + app.getUrls(), + state, + app.getDiskQuota(), + extras + ); + } + + public static CFServiceInstance wrap(final ServiceInstance service) { + return new CFServiceInstance() { + + @Override + public String getName() { + return service.getName(); + } + + @Override + public String getPlan() { + return service.getPlan(); + } + + @Override + public String getDashboardUrl() { + return service.getDashboardUrl(); + } + + @Override + public String getService() { + if (service.getType()==ServiceInstanceType.USER_PROVIDED) { + return "user-provided"; + } else { + return service.getService(); + } + } + + @Override + public String getDescription() { + return service.getDescription(); + } + + @Override + public String getDocumentationUrl() { + return service.getDocumentationUrl(); + } + }; + } + + public static CFAppState wrapAppState(String s) { + try { + return CFAppState.valueOf(s); + } catch (Exception e) { + Log.log(e); + return CFAppState.UNKNOWN; + } + } + + public static CFSpace wrap(OrganizationSummary org, SpaceSummary space) { + return new CFSpace() { + @Override + public String getName() { + return space.getName(); + } + @Override + public CFOrganization getOrganization() { + return wrap(org); + } + @Override + public UUID getGuid() { + return UUID.fromString(space.getId()); + } + + @Override + public String toString() { + return "CFSpace("+org.getName()+" / "+getName()+")"; + } + }; + } + + public static CFOrganization wrap(OrganizationSummary org) { + return new CFOrganization() { + @Override + public String getName() { + return org.getName(); + } + + @Override + public UUID getGuid() { + return UUID.fromString(org.getId()); + } + }; + } + + public static CFBuildpack buildpack(String name) { + return new CFBuildpack() { + @Override + public String getName() { + return name; + } + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CloudFoundryClientCache.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CloudFoundryClientCache.java new file mode 100644 index 000000000..f7709bb4e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/CloudFoundryClientCache.java @@ -0,0 +1,261 @@ +/******************************************************************************* + * Copyright (c) 2016, 2018 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.cf.client.v2; + +import java.net.URL; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.cloudfoundry.client.CloudFoundryClient; +import org.cloudfoundry.client.v2.info.GetInfoRequest; +import org.cloudfoundry.client.v2.info.GetInfoResponse; +import org.cloudfoundry.reactor.DefaultConnectionContext; +import org.cloudfoundry.reactor.ProxyConfiguration; +import org.cloudfoundry.reactor.TokenProvider; +import org.cloudfoundry.reactor.client.ReactorCloudFoundryClient; +import org.cloudfoundry.reactor.doppler.ReactorDopplerClient; +import org.cloudfoundry.reactor.tokenprovider.OneTimePasscodeTokenProvider; +import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider; +import org.cloudfoundry.reactor.tokenprovider.RefreshTokenGrantTokenProvider; +import org.cloudfoundry.reactor.uaa.ReactorUaaClient; +import org.eclipse.core.net.proxy.IProxyData; +import org.eclipse.core.net.proxy.IProxyService; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.frameworks.core.util.StringUtils; + +import reactor.core.publisher.Mono; + +/** + * TODO: Remove this class when the 'thread leak bug' in V2 client is fixed. + * + * At the moment each time {@link SpringCloudFoundryClient} is create a threadpool + * is created by the client and it is never cleaned up. The only way we have + * to mitigate this leak is to try and create as few clients as possible. + *

+ * So we have a permanent cache of clients here that is reused. + *

+ * When the bug is fixed then this should no longer be necessary and we can removed this cache + * and just create the client as needed. + * + * @author Kris De Volder + */ +public class CloudFoundryClientCache { + + public class CFClientProvider { + + final DefaultConnectionContext connection; + final TokenProvider tokenProvider; + + //Note: the three client objects below are 'stateless' wrappers and it would be + // fine to recreate as needed instead of store them + + final CloudFoundryClient client; + final ReactorUaaClient uaaClient; + final ReactorDopplerClient doppler; + final Mono info; + + private ProxyConfiguration getProxy(String host) { + try { + if (StringUtils.hasText(host)) { + URL url = new URL("https://"+host); + // In certain cases, the activator would have stopped and the plugin may + // no longer be available. Usually onl happens on shutdown. + BootDashActivator plugin = BootDashActivator.getDefault(); + if (plugin != null) { + IProxyService proxyService = plugin.getProxyService(); + if (proxyService != null) { + IProxyData[] selectedProxies = proxyService.select(url.toURI()); + + // No proxy configured or not found + if (selectedProxies == null || selectedProxies.length == 0) { + return null; + } + + IProxyData data = selectedProxies[0]; + int proxyPort = data.getPort(); + String proxyHost = data.getHost(); + String user = data.getUserId(); + String password = data.getPassword(); + if (proxyHost!=null) { + return ProxyConfiguration.builder() + .host(proxyHost) + .port(proxyPort==-1?Optional.empty():Optional.of(proxyPort)) + .username(Optional.ofNullable(user)) + .password(Optional.ofNullable(password)) + .build(); +// return proxyHost != null ? new HttpProxyConfiguration(proxyHost, proxyPort, +// data.isRequiresAuthentication(), user, password) : null; + } + } + } + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + public CFClientProvider(Params params) { + long sslTimeout = Long.getLong("sts.bootdash.cf.client.ssl.handshake.timeout", 60); //TODO: make a preference for this? + Optional keepAlive = getBooleanSystemProp("http.keepAlive"); + debug("cf client keepAlive = "+keepAlive); + connection = DefaultConnectionContext.builder() + .proxyConfiguration(Optional.ofNullable(getProxy(params.host))) + .apiHost(params.host) + .sslHandshakeTimeout(Duration.ofSeconds(sslTimeout)) + .keepAlive(keepAlive) + .skipSslValidation(params.skipSsl) + .build(); + + tokenProvider = createTokenProvider(params); + + client = ReactorCloudFoundryClient.builder() + .connectionContext(connection) + .tokenProvider(tokenProvider) + .build(); + + uaaClient = ReactorUaaClient.builder() + .connectionContext(connection) + .tokenProvider(tokenProvider) + .build(); + + doppler = ReactorDopplerClient.builder() + .connectionContext(connection) + .tokenProvider(tokenProvider) + .build(); + + // Cache CF client info - workaround for https://www.pivotaltracker.com/story/show/158741609 + info = client.info().get(GetInfoRequest.builder().build()).cache(); + } + + private TokenProvider createTokenProvider(Params params) { + CFCredentials creds = params.credentials; + switch (creds.getType()) { + case PASSWORD: + return PasswordGrantTokenProvider.builder() + .username(params.username) + .password(creds.getSecret()) + .build(); + case REFRESH_TOKEN: + return RefreshTokenGrantTokenProvider.builder() + .token(creds.getSecret()) + .build(); + case TEMPORARY_CODE: + return OneTimePasscodeTokenProvider.builder() + .passcode(creds.getSecret()) + .build(); + default: + throw new IllegalStateException("BUG! Missing switch case?"); + } + } + + private Optional getBooleanSystemProp(String name) { + String str = System.getProperty(name); + if (str!=null) { + return Optional.of(Boolean.valueOf(str)); + } + return Optional.empty(); + } + } + + private static final boolean DEBUG = true; + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + public static class Params { + public final String username; + public final CFCredentials credentials; + public final String host; + public final boolean skipSsl; + public Params(String username, CFCredentials credentials, String host, boolean skipSsl) { + super(); + this.username = username; + this.credentials = credentials; + this.host = host; + this.skipSsl = skipSsl; + } + + @Override + public String toString() { + return "Params [username=" + username + ", host=" + host + ", skipSsl=" + skipSsl + + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((host == null) ? 0 : host.hashCode()); + result = prime * result + ((credentials == null) ? 0 : credentials.hashCode()); + result = prime * result + (skipSsl ? 1231 : 1237); + result = prime * result + ((username == null) ? 0 : username.hashCode()); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Params other = (Params) obj; + if (host == null) { + if (other.host != null) + return false; + } else if (!host.equals(other.host)) + return false; + if (credentials == null) { + if (other.credentials != null) + return false; + } else if (!credentials.equals(other.credentials)) + return false; + if (skipSsl != other.skipSsl) + return false; + if (username == null) { + if (other.username != null) + return false; + } else if (!username.equals(other.username)) + return false; + return true; + } + } + + private Map cache = new HashMap<>(); + + private int clientCount = 0; + + public synchronized CFClientProvider getOrCreate(String username, CFCredentials credentials, String host, boolean skipSsl) { + Params params = new Params(username, credentials, host, skipSsl); + CFClientProvider client = cache.get(params); + if (client==null) { + clientCount++; + debug("Creating client ["+clientCount+"]: "+params); + cache.put(params, client = create(params)); + } else { + debug("Reusing client ["+clientCount+"]: "+params); + } + return client; + } + + protected CFClientProvider create(Params params) { + return new CFClientProvider(params); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/DefaultClientRequestsV2.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/DefaultClientRequestsV2.java new file mode 100644 index 000000000..edae43a2d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/DefaultClientRequestsV2.java @@ -0,0 +1,1376 @@ +/******************************************************************************* + * Copyright (c) 2016, 2018 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.cf.client.v2; + +import java.io.IOException; +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.apache.commons.lang3.RandomStringUtils; +import org.cloudfoundry.client.CloudFoundryClient; +import org.cloudfoundry.client.v2.applications.ApplicationEntity; +import org.cloudfoundry.client.v2.applications.GetApplicationResponse; +import org.cloudfoundry.client.v2.applications.UpdateApplicationRequest; +import org.cloudfoundry.client.v2.applications.UpdateApplicationResponse; +import org.cloudfoundry.client.v2.buildpacks.ListBuildpacksRequest; +import org.cloudfoundry.client.v2.buildpacks.ListBuildpacksResponse; +import org.cloudfoundry.client.v2.info.GetInfoRequest; +import org.cloudfoundry.client.v2.info.GetInfoResponse; +import org.cloudfoundry.client.v2.serviceinstances.DeleteServiceInstanceRequest; +import org.cloudfoundry.client.v2.stacks.GetStackRequest; +import org.cloudfoundry.client.v2.stacks.GetStackResponse; +import org.cloudfoundry.client.v2.userprovidedserviceinstances.DeleteUserProvidedServiceInstanceRequest; +import org.cloudfoundry.client.v2.users.GetUserRequest; +import org.cloudfoundry.doppler.LogMessage; +import org.cloudfoundry.doppler.MessageType; +import org.cloudfoundry.operations.CloudFoundryOperations; +import org.cloudfoundry.operations.DefaultCloudFoundryOperations; +import org.cloudfoundry.operations.applications.ApplicationDetail; +import org.cloudfoundry.operations.applications.ApplicationHealthCheck; +import org.cloudfoundry.operations.applications.ApplicationManifest; +import org.cloudfoundry.operations.applications.DeleteApplicationRequest; +import org.cloudfoundry.operations.applications.GetApplicationEnvironmentsRequest; +import org.cloudfoundry.operations.applications.GetApplicationRequest; +import org.cloudfoundry.operations.applications.LogsRequest; +import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; +import org.cloudfoundry.operations.applications.RestartApplicationRequest; +import org.cloudfoundry.operations.applications.StartApplicationRequest; +import org.cloudfoundry.operations.applications.StopApplicationRequest; +import org.cloudfoundry.operations.domains.Domain; +import org.cloudfoundry.operations.organizations.OrganizationDetail; +import org.cloudfoundry.operations.organizations.OrganizationInfoRequest; +import org.cloudfoundry.operations.organizations.OrganizationSummary; +import org.cloudfoundry.operations.routes.Level; +import org.cloudfoundry.operations.routes.ListRoutesRequest; +import org.cloudfoundry.operations.routes.MapRouteRequest; +import org.cloudfoundry.operations.routes.UnmapRouteRequest; +import org.cloudfoundry.operations.services.BindServiceInstanceRequest; +import org.cloudfoundry.operations.services.CreateServiceInstanceRequest; +import org.cloudfoundry.operations.services.CreateUserProvidedServiceInstanceRequest; +import org.cloudfoundry.operations.services.GetServiceInstanceRequest; +import org.cloudfoundry.operations.services.ServiceInstance; +import org.cloudfoundry.operations.services.ServiceInstanceSummary; +import org.cloudfoundry.operations.services.UnbindServiceInstanceRequest; +import org.cloudfoundry.reactor.DefaultConnectionContext; +import org.cloudfoundry.reactor.tokenprovider.AbstractUaaTokenProvider; +import org.cloudfoundry.uaa.UaaClient; +import org.cloudfoundry.util.PaginationUtils; +import org.eclipse.core.runtime.Platform; +import org.osgi.framework.Version; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplicationDetail; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFBuildpack; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFClientParams; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFDomainType; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFServiceInstance; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFSpace; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFStack; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.SshClientSupport; +import org.springframework.ide.eclipse.boot.dash.cf.client.SshHost; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CloudFoundryClientCache.CFClientProvider; +import org.springframework.ide.eclipse.boot.dash.console.IApplicationLogConsole; +import org.springframework.ide.eclipse.boot.dash.console.LogType; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; + +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Kris De Volder + * @author Nieraj Singh + */ +public class DefaultClientRequestsV2 implements ClientRequests { + + private static AtomicLong instances = new AtomicLong(0); + + public static final Duration APP_START_TIMEOUT = Duration.ofMinutes(10); + public static final Duration GET_SERVICES_TIMEOUT = Duration.ofSeconds(60); + public static final Duration GET_SPACES_TIMEOUT = Duration.ofSeconds(20); + public static final Duration GET_USERNAME_TIMEOUT = Duration.ofSeconds(5); + public static final Duration GET_SMALL_INFO_TIMEOUT = Duration.ofSeconds(20); + + private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder") || (""+Platform.getLocation()).contains("bamboo"); +// private static final boolean DEBUG_REACTOR = (""+Platform.getLocation()).contains("kdvolder"); + //|| (""+Platform.getLocation()).contains("bamboo"); + + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + +// static { +// if (DEBUG_REACTOR) { +// Loggers.enableExtension(new Extension() { +// @Override +// public void log(String category, java.util.logging.Level level, String msg, Object... arguments) { +// debug(category +"["+level + "] : "+MessageFormatter.format(msg, arguments).getMessage()); +// } +// }); +// } +// } + + + private CFClientParams params; + private CloudFoundryClient _client ; + private UaaClient _uaa; + private CloudFoundryOperations _operations; + + private Mono orgId; + private Mono info; + private AbstractUaaTokenProvider _tokenProvider; + private DefaultConnectionContext _connection; + private String refreshToken = null; + private Flux refreshTokensFlux; + + private CompletableFuture _disposed = new CompletableFuture<>(); + + public DefaultClientRequestsV2(CloudFoundryClientCache clients, CFClientParams params) { + this.params = params; + CFClientProvider provider = clients.getOrCreate(params.getUsername(), params.getCredentials(), params.getHost(), params.skipSslValidation()); + this._client = provider.client; + this._uaa = provider.uaaClient; + this._tokenProvider = (AbstractUaaTokenProvider) provider.tokenProvider; + this._connection = provider.connection; + refreshTokensFlux = _tokenProvider.getRefreshTokens(_connection).takeUntilOther(Mono.fromFuture(_disposed)); + refreshTokensFlux.doOnNext((t) -> { + this.refreshToken = t; + }).subscribe(); + + debug(">>> creating cf operations"); + this._operations = DefaultCloudFoundryOperations.builder() + .cloudFoundryClient(_client) + .dopplerClient(provider.doppler) + .uaaClient(provider.uaaClient) + .organization(params.getOrgName()) + .space(params.getSpaceName()) + .build(); + debug("<<< creating cf operations"); + this.orgId = getOrgId(); + // Use cached info, workaround for https://www.pivotaltracker.com/story/show/158741609 + this.info = provider.info; + debug("DefaultClientRequestsV2 created: "+instances.incrementAndGet()); + } + + private Mono client_createOperations(OrganizationSummary org) { + return log("client.createOperations(org="+org.getName()+")", + Mono.fromCallable(() -> DefaultCloudFoundryOperations.builder() + .cloudFoundryClient(_client) + .organization(org.getName()) + .build() + ) + ); + } + + private Mono getOrgId() { + String orgName = params.getOrgName(); + if (orgName==null) { + return Mono.error(new IOException("No organization targetted")); + } else { + return operations_getOrgId().cache(); + } + } + + @Override + public List getApplicationsWithBasicInfo() throws Exception { + return ReactorUtils.get(operations_listApps()); + } + + private ApplicationExtras getApplicationExtras(String appName) { + //Stuff used in computing the 'extras'... + Mono appIdMono = getApplicationId(appName); + Mono entity = appIdMono + .flatMap((appId) -> + client_getApplication(appId) + ) + .map((appResource) -> appResource.getEntity()) + .cache(); + + //The stuff returned from the getters of 'extras'... + Mono> services = prefetch("services", getBoundServicesList(appName)); + Mono> env = prefetch("env", + DefaultClientRequestsV2.this.getEnv(appName) + ); + Mono buildpack = prefetch("buildpack", + entity.flatMap((e) -> Mono.justOrEmpty(e.getBuildpack())) + ); + + Mono stack = prefetch("stack", + entity.flatMap((e) -> Mono.justOrEmpty(e.getStackId())) + .flatMap((stackId) -> { + return client_getStack(stackId); + }).map((response) -> { + return response.getEntity().getName(); + }) + ); + Mono timeout = prefetch("timeout", + entity + .flatMap((v) -> Mono.justOrEmpty(v.getHealthCheckTimeout())) + ); + + Mono command = prefetch("command", + entity.flatMap((e) -> Mono.justOrEmpty(e.getCommand())) + ); + + Mono healthCheckType = prefetch("healthCheckType", + entity.flatMap((e) -> Mono.justOrEmpty(e.getHealthCheckType())) + ); + + Mono healthCheckHttpEndpoint = prefetch("healthCheckHttpEndpoint", + entity.flatMap((e) -> Mono.justOrEmpty(e.getHealthCheckHttpEndpoint())) + ); + + return new ApplicationExtras() { + @Override + public Mono> getServices() { + return services; + } + + @Override + public Mono> getEnv() { + return env; + } + + @Override + public Mono getBuildpack() { + return buildpack; + } + + @Override + public Mono getStack() { + return stack; + } + + @Override + public Mono getTimeout() { + return timeout; + } + + @Override + public Mono getCommand() { + return command; + } + + @Override + public Mono getHealthCheckType() { + return healthCheckType; + } + + @Override + public Mono getHealthCheckHttpEndpoint() { + return healthCheckHttpEndpoint; + } + }; + } + + private Mono prefetch(String id, Mono toFetch) { + return toFetch +// .log(id + " before error handler") + .onErrorResume((error) -> { + Log.log(new IOException("Failed prefetch '"+id+"'", error)); + return Mono.empty(); + }) +// .log(id + " after error handler") + .cache() +// .log(id + "after cache") + ; + } + +// private Mono prefetch(Mono toFetch) { +// Mono result = toFetch +// .cache(); // It should only be fetched once. +// +// //We must ensure the 'result' is being consumed by something to force its execution: +// result +// .publishOn(SCHEDULER_GROUP) //Ensure the consume is truly async or it may block here. +// .consume((dont_care) -> {}); +// +// return result; +// } + + @Override + public List getServices() throws Exception { + return ReactorUtils.get(GET_SERVICES_TIMEOUT, CancelationTokens.NULL, + log("operations.services.listInstances()", + _operations + .services() + .listInstances() + .flatMap(this::getServiceDetails) + .map(CFWrappingV2::wrap) + .collectList() + .map(ImmutableList::copyOf) + ) + ); + } + + + private Mono getServiceDetails(ServiceInstanceSummary summary) { + return log("operations.service.getServiceInstance", + _operations.services().getInstance(GetServiceInstanceRequest.builder() + .name(summary.getName()) + .build() + ) + ); + } + /** + * Get details for a given list of applications. This does a 'best' effort getting the details for + * as many apps as possible but it does not guarantee that it will return details for each app in the + * list. This is to avoid one 'bad apple' from spoiling the whole batch. (I.e if failing to fetch details for + * some apps we can still return details for the others rather than throw an exception). + */ + @Override + public Flux getApplicationDetails(List appsToLookUp) throws Exception { + return Flux.fromIterable(appsToLookUp) + .flatMap((CFApplication appSummary) -> { + return getApplicationDetail(appSummary.getName()) + .onErrorResume((error) -> { + Log.log(ExceptionUtil.coreException("getting application details for '"+appSummary.getName()+"' failed", error)); + return Mono.empty(); + }) + .map((ApplicationDetail appDetails) -> CFWrappingV2.wrap((CFApplicationSummaryData)appSummary, appDetails)); + }); + } + + @Override + public Disposable streamLogs(String appName, IApplicationLogConsole logConsole) throws Exception { + Flux stream = log("operations.applications.logs()", + _operations.applications() + .logs(LogsRequest.builder() + .name(appName) + // BUG: show recent appears to throw exception with PWS. May be fixed in the future, but now only "pure" streaming is supported + .recent(false) + .build() + ) + ) + .retryWhen(retryInterval(Duration.ofMillis(500), Duration.ofMinutes(1))) + ; + + Disposable cancellation = ReactorUtils.sort( + stream, + (m1, m2) -> Long.compare(m1.getTimestamp(), m2.getTimestamp()), + Duration.ofSeconds(1) + ) + .map(this::convertMessageFromDoppler) + .subscribe(logConsole::onMessage, logConsole::onError); + + return cancellation; + } + + + private org.springframework.ide.eclipse.boot.dash.console.LogMessage convertMessageFromDoppler(LogMessage msg) { + return new org.springframework.ide.eclipse.boot.dash.console.LogMessage( + convertMessageTypeFromDoppler(msg.getMessageType()), + msg.getMessage() + ); + } + + private LogType convertMessageTypeFromDoppler(MessageType mt) { + switch (mt) { + case OUT: + return LogType.APP_OUT; + case ERR: + return LogType.APP_ERROR; + default: + return LogType.APP_OUT; + } + } + /** + * Creates a retry 'signal factory' to be used with Flux.retryWhen. + *

+ * @param timeBetween How much time to wait before retrying after a failure + * @param duration If this much time has elapsed when the error happens there will be no further retries. + * @return Functon that can be passed to retryWhen. + */ + private Function, Flux> retryInterval(Duration timeBetween, Duration duration) { + Predicate falseAfterDuration = falseAfter(duration); + return (errors) -> { + return errors.flatMap((error) -> { + if (falseAfterDuration.test(error)) { + return Mono.delay(timeBetween); + } else { + return Mono.error(error); + } + }); + }; + } + + private Predicate falseAfter(Duration timeToWait) { + return new Predicate() { + + private Long firstCalledAt; + + @Override + public boolean test(Throwable t) { + if (firstCalledAt==null) { + firstCalledAt = System.currentTimeMillis(); + } + long waitedTime = System.currentTimeMillis() - firstCalledAt; + //debug("falseAfter: remaining = "+(timeToWait.toMillis() - waitedTime)); + return waitedTime < timeToWait.toMillis(); + } + + }; + } + + @Override + public void stopApplication(String appName) throws Exception { + ReactorUtils.get( + stopApp(appName) + ); + } + + private Mono stopApp(String appName) { + return log("operations.applications.stop(name="+appName+")", + _operations.applications().stop(StopApplicationRequest.builder() + .name(appName) + .build() + ) + ); + } + + @Override + public void restartApplication(String appName, CancelationToken cancelationToken) throws Exception { + ReactorUtils.get(APP_START_TIMEOUT, cancelationToken, + restartApp(appName) + ); + } + + private Mono restartApp(String appName) { + return log("operations.applications().restart(name="+appName+")", + _operations.applications().restart(RestartApplicationRequest.builder() + .name(appName) + .build()) + ); + } + + @Override + public void dispose() { + synchronized (this) { + if (_client!=null) { + _client = null; + _connection = null; + _operations = null; + _disposed.complete(true); + debug("DefaultClientRequestsV2.logout: "+instances.decrementAndGet()); + } + } + } + + public boolean isLoggedOut() { + return _client==null; + } + + @Override + public List getStacks() throws Exception { + return ReactorUtils.get( + log("operations.stacks().list()", + _operations.stacks() + .list() + .map(CFWrappingV2::wrap) + .collectList() + ) + ); + } + + @Override + public SshClientSupport getSshClientSupport() throws Exception { + return new SshClientSupport() { + + @Override + public String getSshUser(UUID appGuid, int instance) { + return "cf:"+appGuid+"/" + instance; + } + + @Override + public String getSshUser(String appName, int instance) throws Exception { + return ReactorUtils.get( + getApplicationId(appName) + .map((guid) -> getSshUser(guid, instance)) + ); + } + + @Override + public SshHost getSshHost() throws Exception { + return ReactorUtils.get( + info.flatMap((i) -> { + String fingerPrint = i.getApplicationSshHostKeyFingerprint(); + String host = i.getApplicationSshEndpoint(); + int port = 22; //Default ssh port + if (host!=null) { + if (host.contains(":")) { + String[] pieces = host.split(":"); + host = pieces[0]; + port = Integer.parseInt(pieces[1]); + } + } + if (host!=null) { + return Mono.just(new SshHost(host, port, fingerPrint)); + } + // Workaround for bug in Eclipse Neon.1 JDT cannot properly infer type SshHost. + // Works in Mars. Returning Mono.empty() results in compilation error in Neon.1 + return Mono.empty(); + }) + ); + } + + @Override + public String getSshCode() throws Exception { + return ReactorUtils.get( + log("operations.advanced.sshCode()", + _operations.advanced().sshCode() + ) + ); + } + }; + } + + private Mono operationsFor(OrganizationSummary org) { + return client_createOperations(org); + } + + @Override + public List getSpaces() throws Exception { + Object it = ReactorUtils.get(GET_SPACES_TIMEOUT, log("operations.organizations().list()", + _operations.organizations() + .list() + ) + .flatMap((OrganizationSummary org) -> { + return operationsFor(org).flatMapMany((operations) -> + log("operations.spaces.list(org="+org.getId()+")", + operations + .spaces() + .list() + .map((space) -> CFWrappingV2.wrap(org, space)) + ) + ); + }) + .collectList() + ); + //workaround eclipse jdt bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=501949 + return (List) it; + } + + @Override + public String getHealthCheck(UUID appGuid) throws Exception { + //XXX CF V2: getHealthcheck (via operations API) + // See: https://www.pivotaltracker.com/story/show/116462215 + return ReactorUtils.get( + client_getApplication(appGuid) + .map((response) -> response.getEntity().getHealthCheckType()) + ); + } + + @Override + public void setHealthCheck(UUID guid, String hcType) throws Exception { + //XXX CF V2: setHealthCheck (via operations API) + // See: https://www.pivotaltracker.com/story/show/116462369 + ReactorUtils.get( + client_setHealthCheck(guid, hcType) + ); + } + + @Override + public List getDomains() throws Exception { + return getDomainsOracle() + .getDomainsList(); + } + + private DomainsOracle getDomainsOracle() { + return new DomainsOracle(); + } + + /** + * DomainsOracle answers questions about domains. It only reads the domain information from + * CF once. (However, every newly created instance of DomainsOracle will read the info the + * first time it needs it. + */ + private class DomainsOracle { + private Mono> domains = orgId + .flatMapMany(this::requestDomains) + .map(CFWrappingV2::wrap) + .collectList() + .cache(); + + private Mono> domainsByName = domains + .map(allDomains -> { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (CFCloudDomain d : allDomains) { + builder.put(d.getName(), d); + } + return (Map)builder.build(); + }) + .cache(); + + public Mono> getDomainsMono() { + return domains; + } + + public List getDomainsList() throws Exception { + return ReactorUtils.get(domains); + } + + private Flux requestDomains(String orgId) { + return log("operations.domains.list", + _operations.domains().list() + ); + } + + public Mono isTcp(String domainName) { + return domainsByName.flatMap(dmap -> { + CFCloudDomain domain = dmap.get(domainName); + return Mono.just(domain!=null && domain.getType()==CFDomainType.TCP); + }); + } + } + + @Override + public List getBuildpacks() throws Exception { + //XXX CF V2: getBuilpacks using 'operations' API. + return ReactorUtils.get( + PaginationUtils.requestClientV2Resources((page) -> { + return client_listBuildpacks(page); + }) + .map(CFWrappingV2::wrap) + .collectList() + ); + } + + @Override + public CFApplicationDetail getApplication(String appName) throws Exception { + return ReactorUtils.get( + getApplicationMono(appName) + // .log("getApplication("+appName+")") + ); + } + + private Mono getApplicationMono(String appName) { + return getApplicationDetail(appName) + .map((appDetail) -> { + //TODO: we have 'real' appdetails now so we could get most of the 'application extras' info from that. + return CFWrappingV2.wrap(appDetail, getApplicationExtras(appName)); + }) + .onErrorResume(ReactorUtils.suppressException(IllegalArgumentException.class)); + } + + @Override + public Version getApiVersion() throws Exception { + return ReactorUtils.get(info + .map((i) -> new Version(i.getApiVersion())) + ); + } + + @Override + public Version getSupportedApiVersion() { + return new Version(CloudFoundryClient.SUPPORTED_API_VERSION); + } + + @Override + public void deleteApplication(String appName) throws Exception { + ReactorUtils.get( + log("operations.applications().delete(name="+appName+")", + _operations.applications().delete(DeleteApplicationRequest + .builder() + .name(appName) + .build() + ) + ) + ); + } + + @Override + public boolean applicationExists(String appName) throws Exception { + return ReactorUtils.get( + getApplicationMono(appName) + .map((app) -> true) + .switchIfEmpty(Mono.just(false)) + ); + } + + public Mono ifApplicationExists(String appName, Function> then, Mono els) throws Exception { + return getApplicationDetail(appName) + .map((app) -> Optional.of(app)) + .switchIfEmpty(Mono.just(Optional.empty())) + .onErrorResume((error) -> Mono.just(Optional.empty())) + .flatMap((Optional app) -> { + if (app.isPresent()) { + return then.apply(app.get()); + } else { + return els; + } + }); + } + + @Override + public void push(CFPushArguments params, CancelationToken cancelationToken) throws Exception { + ReactorUtils.get(APP_START_TIMEOUT, cancelationToken, + v2Push(params) + ); + } + + private Mono v2Push(CFPushArguments params) { + String appName = params.getAppName(); + + // Routes are set AFTER push, so for initial push, make sure no route is set + boolean noRoute = true; + + // Environment variables require app start, so for initial push, do not start app. + // This will happen afterwards + boolean noStart = true; + PushApplicationManifestRequest req = PushApplicationManifestRequest.builder() + .manifest(ApplicationManifest.builder() + .name(appName) + // resource matching occurs under the hood in the push operation + .path(params.getApplicationDataAsFile().toPath()) + .memory(params.getMemory()) + .disk(params.getDiskQuota()) + .timeout(params.getTimeout()) + .healthCheckType(resolveHealthCheckType(params.getHealthCheckType()).orElse(null)) + .healthCheckHttpEndpoint(params.getHealthCheckHttpEndpoint()==null?"/":params.getHealthCheckHttpEndpoint()) + .buildpack(params.getBuildpack()) + .command(params.getCommand()) + .stack(params.getStack()) + .noRoute(noRoute) + .instances(params.getInstances()) + .build() + ) + .noStart(noStart) + .build(); + + return log("client.applications.pushManifest("+req+")", + _operations.applications().pushManifest(req) + ) + .then(mono_debug("Updating routes, bound services, and environment variables...")) + .then(getApplicationDetail(appName)) + .flatMap((appDetail) -> { + return Flux.merge( + setRoutes(appDetail, params.getRoutes(), params.getRandomRoute()), + bindAndUnbindServices(appName, params.getServices()), + // This requires app restart + setEnvVars(appDetail, params.getEnv()) + ).then(); + }) + // Start app only after environment variables are set + .then(params.isNoStart() + ? stopApp(appName) + : restartApp(appName) + ) + .then(Mono.empty()); + } + + private Mono mono_debug(String string) { + return Mono.fromRunnable(() -> debug(string)); + } + + private Mono> getStackId(CFPushArguments params) { + String stackName = params.getStack(); + if (stackName==null) { + return Mono.just(Optional.empty()); + } else { + return log("operations.stacks.get("+stackName+")", + _operations.stacks().get(org.cloudfoundry.operations.stacks.GetStackRequest.builder() + .name(stackName) + .build() + ) + .map((stack) -> Optional.of(stack.getId())) + ); + } + } + + private Optional resolveHealthCheckType(String type) { + ApplicationHealthCheck appHealthCheck = null; + if (type != null) { + try { + appHealthCheck = ApplicationHealthCheck.from(type); + } catch (IllegalArgumentException e) { + Log.log(e); + } + } + return Optional.ofNullable(appHealthCheck); + } + + private Mono getApplicationDetail(String appName) { + return log("operations.applications.get(name="+appName+")", + _operations.applications().get(GetApplicationRequest.builder() + .name(appName) + .build() + ) + ); + } + + public Mono setRoutes(ApplicationDetail appDetails, Collection desiredUrls, boolean randomRoute) { + debug("setting routes for '"+appDetails.getName()+"': "+desiredUrls+", "+randomRoute); + DomainsOracle domains = new DomainsOracle(); + + //Carefull! It is not safe map/unnmap multiple routes in parallel. Doing so causes some of the + // operations to fail, presumably because of some 'optimisitic locking' being used in the database + // that keeps track of routes. + //To avoid this problem we must execute all that map / unmap calls in sequence! + return ReactorUtils.sequence( + unmapUndesiredRoutes(appDetails.getName(), desiredUrls), + mapDesiredRoutes(appDetails, domains, desiredUrls, randomRoute) + ); + } + + public Mono setRoutes(String appName, Collection desiredUrls, boolean randomRoute) { + return getApplicationDetail(appName) + .flatMap(appDetails -> setRoutes(appDetails, desiredUrls, randomRoute)); + } + + private Mono mapDesiredRoutes(ApplicationDetail appDetail, DomainsOracle domains, Collection desiredUrls, boolean randomRoute) { + Set currentUrls = ImmutableSet.copyOf(appDetail.getUrls()); + String appName = appDetail.getName(); + + debug("currentUrls = "+currentUrls); + return Flux.fromIterable(desiredUrls) + .flatMap((url) -> { + if (currentUrls.contains(url)) { + debug("skipping: "+url); + return Mono.empty(); + } else { + debug("mapping: "+url); + return mapRoute(domains, appName, url, randomRoute); + } + }, 1) //!!!IN SEQUENCE!!! + .then(); + } + + private Mono mapRoute(DomainsOracle domains, String appName, String desiredUrl, boolean randomRoute) { + debug("mapRoute: "+appName+" -> "+desiredUrl); + return toRoute(domains, desiredUrl) + .flatMap((CFRoute route) -> mapRoute(domains, appName, route, randomRoute)) + .doOnError((e) -> { + Log.info("mapRoute FAILED!"); + Log.log(e); + }); + } + + private Mono mapRoute(DomainsOracle domains, String appName, CFRoute route, boolean randomRoute) { + // Let the client validate if any of these combinations are correct. + // However, only set these values only if they are present as not doing so causes NPE + Mono _builder = Mono.just(MapRouteRequest.builder() + .applicationName(appName) + ); + if (StringUtil.hasText(route.getDomain())) { + _builder = _builder.map(builder -> builder.domain(route.getDomain())); + } + if (randomRoute) { + _builder = _builder.flatMap((MapRouteRequest.Builder builder) -> + // Can only set random port to true if route is TCP route + domains.isTcp(route.getDomain()).map(isTcp -> { + if (isTcp) { + builder.randomPort(randomRoute); + } else { + if (route.getHost()==null) { + builder.host(appName+"-"+RandomStringUtils.randomAlphabetic(8).toLowerCase()); + } + } + return builder; + }) + ); + } + _builder = _builder.map(builder -> { + if (StringUtil.hasText(route.getHost())) { + builder.host(route.getHost()); + } + if (StringUtil.hasText(route.getPath())) { + builder.path(route.getPath()); + } + if (route.getPort() != CFRoute.NO_PORT) { + builder.port(route.getPort()); + } + return builder; + }); + return _builder.flatMap(builder -> { + MapRouteRequest mapRouteReq = builder.build(); + return log("operations.routes.map("+mapRouteReq+")", + _operations.routes().map(mapRouteReq) + ); + }) + .then(); + } + + private Mono toRoute(DomainsOracle domains, String desiredUrl) { + return domains.getDomainsMono().flatMap((ds) -> { + try { + CFRoute route = CFRoute.builder().from(desiredUrl, ds).build(); + route.validate(); + return Mono.just(route); + } catch (Exception e) { + return Mono.error(e); + } + }); + } + +// private Set getUrls(ApplicationDetail app) { +// return operations.applications().get(GetApplicationRequest.builder() +// .name(appName) +// .build() +// ) +// .map((app) -> ImmutableSet.copyOf(app.getUrls())); +// } + + private Mono unmapUndesiredRoutes(String appName, Collection desiredUrls) { + return getExistingRoutes(appName) + .flatMap((route) -> { + debug("unmap? "+route); + if (desiredUrls.contains(getUrl(route))) { + debug("unmap? "+route+" SKIP"); + return Mono.empty(); + } else { + debug("unmap? "+route+" UNMAP"); + return unmapRoute(appName, route); + } + }, 1) //!!!IN SEQUENCE!!! + .then(); + } + + private String getUrl(CFRoute route) { +// String url = route.getDomain(); +// if (route.getHost()!=null) { +// url = route.getHost() + "." + url; +// } +// String path = route.getPath(); +// if (path!=null) { +// while (path.startsWith("/")) { +// path = path.substring(1); +// } +// if (StringUtils.hasText(path)) { +// url = url +"/" +path; +// } +// } + return route.getRoute(); + } + + private Mono unmapRoute(String appName, CFRoute route) { +// if (!StringUtil.hasText(path)) { +// //client doesn't like to get 'empty string' it will complain that route doesn't exist. +// path = null; +// } + org.cloudfoundry.operations.routes.UnmapRouteRequest.Builder unmapBuilder = UnmapRouteRequest.builder() + .applicationName(appName) + .domain(route.getDomain()) + .host(route.getHost()); + + // Have to check if port and path are set. Cannot just set them without checking + // otherwise exception throw, even if these values are "empty/null" in the route + if (route.getPort() != CFRoute.NO_PORT) { + unmapBuilder.port(route.getPort()); + } + if (StringUtil.hasText(route.getPath())) { + unmapBuilder.path(route.getPath()); + } + UnmapRouteRequest req = unmapBuilder + .build(); + return log("operations.routes.unmap("+req+")", + _operations.routes().unmap(req) + ); + } + + public Flux getExistingRoutes(String appName) { + return log("operations.routes.list(level=SPACE)", + _operations.routes().list(ListRoutesRequest.builder() + .level(Level.SPACE) + .build() + ) + ) + .flatMap((route) -> { + for (String app : route.getApplications()) { + if (app.equals(appName)) { + return Mono.just(CFRoute.builder().from(route).build()); + } + }; + return Mono.empty(); + }); + } + + public Mono bindAndUnbindServices(String appName, List _services) { + debug("bindAndUnbindServices "+_services); + Set services = ImmutableSet.copyOf(_services); + return getBoundServicesSet(appName) + .flatMapMany((boundServices) -> { + debug("boundServices = "+boundServices); + Set toUnbind = Sets.difference(boundServices, services); + Set toBind = Sets.difference(services, boundServices); + debug("toBind = "+toBind); + debug("toUnbind = "+toUnbind); + return Flux.merge( + bindServices(appName, toBind), + unbindServices(appName, toUnbind) + ); + }) + .then(); + } + + public Flux getBoundServices(String appName) { + return log("operations.services.listInstances()", + _operations.services().listInstances() + ) + .filter((service) -> isBoundTo(service, appName)) + .map(ServiceInstanceSummary::getName); + } + + public Mono> getBoundServicesSet(String appName) { + return getBoundServices(appName) + .collectList() + .map(ImmutableSet::copyOf); + } + + public Mono> getBoundServicesList(String appName) { + return getBoundServices(appName) + .collectList() + .map(ImmutableList::copyOf); + } + + private boolean isBoundTo(ServiceInstanceSummary service, String appName) { + return service.getApplications().stream() + .anyMatch((boundAppName) -> boundAppName.equals(appName)); + } + + private Flux bindServices(String appName, Set services) { + return Flux.fromIterable(services) + .flatMap((service) -> { + return log("operations.services().bind(appName="+appName+", services="+services+")", + _operations.services().bind(BindServiceInstanceRequest.builder() + .applicationName(appName) + .serviceInstanceName(service) + .build() + ) + ); + }); + } + + private Flux unbindServices(String appName, Set toUnbind) { + return Flux.fromIterable(toUnbind) + .flatMap((service) -> { + return log("operations.services.unbind(appName="+appName+", serviceInstanceName="+service+")", + _operations.services().unbind(UnbindServiceInstanceRequest.builder() + .applicationName(appName) + .serviceInstanceName(service) + .build() + ) + ); + }); + } + + + protected Mono startApp(String appName) { + return log("operations.applications.start(name="+appName+")", + _operations.applications() + .start(StartApplicationRequest.builder() + .name(appName) + .build() + ) + ); + } + + private Mono getApplicationId(String appName) { + return log("operations.applications.get(name="+appName+")", + _operations.applications().get(GetApplicationRequest.builder() + .name(appName) + .build() + ) + ).map((app) -> UUID.fromString(app.getId())); + } + + public Mono setEnvVars(ApplicationDetail appDetail, Map environment) { + return setEnvVars(UUID.fromString(appDetail.getId()), environment); + } + + public Mono setEnvVars(UUID appId, Map environment) { + return client_setEnv(appId, environment); + } + + public Mono setEnvVars(String appName, Map environment) { + return getApplicationId(appName) + .flatMap((applicationId) -> setEnvVars(applicationId, environment)); + } + +// protected Publisher setEnvVar(String appName, String var, String value) { +// System.out.println("Set var starting: "+var +" = "+value); +// return operations.applications() +// .setEnvironmentVariable(SetEnvironmentVariableApplicationRequest.builder() +// .name(appName) +// .variableName(var) +// .variableValue(value) +// .build() +// ) +// .after(() -> { +// System.out.println("Set var complete: "+var +" = "+value); +// return Mono.empty(); +// }); +// } + + public Mono createService(String name, String service, String plan) { + return log("operations.services.createInstance(instanceName="+name+",serviceName="+service+",planName="+plan+")", + _operations.services().createInstance(CreateServiceInstanceRequest.builder() + .serviceInstanceName(name) + .serviceName(service) + .planName(plan) + .build() + ) + ); + } + + public Mono createUserProvidedService(String name, Map credentials) { + return log("operations.services.createUserProvidedInstance(name="+name+")", + _operations.services().createUserProvidedInstance(CreateUserProvidedServiceInstanceRequest.builder() + .name(name) + .credentials(credentials) + .build() + ) + ); + } + +// @Override +// public void deleteService(String serviceName) { +// deleteServiceMono(serviceName).get(); +// } + + @Override + public Mono deleteServiceAsync(String serviceName) { + return getService(serviceName) + .flatMap(this::deleteServiceInstance); + } + + protected Mono deleteServiceInstance(ServiceInstance s) { + switch (s.getType()) { + case MANAGED: + return client_deleteServiceInstance(s); + case USER_PROVIDED: + return client_deleteUserProvidedService(s); + default: + return Mono.error(new IllegalStateException("Unknown service type: "+s.getType())); + } + } + + protected Mono getService(String serviceName) { + return log("operations.services.getInstance(name="+serviceName+")", + _operations.services().getInstance(GetServiceInstanceRequest.builder() + .name(serviceName) + .build() + ) + ); + } + + public Mono> getEnv(String appName) { + return log("operations.applications.getEnvironments(appName="+appName+")", + _operations.applications().getEnvironments(GetApplicationEnvironmentsRequest.builder() + .name(appName) + .build() + ) + ) + .map((envs) -> envs.getUserProvided()) + .map(this::dropObjectsFromMap); + } + + @Override + public Map getApplicationEnvironment(String appName) throws Exception { + return ReactorUtils.get(getEnv(appName)); + } + + private Map dropObjectsFromMap(Map map) { + Builder builder = ImmutableMap.builder(); + for (Entry entry : map.entrySet()) { + try { + builder.put(entry.getKey(), (String) entry.getValue()); + } catch (ClassCastException e) { + Log.log(e); + } + } + return builder.build(); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + //// calls to client and operations with 'logging'. + + private Flux log(String msg, Flux flux) { + if (DEBUG) { + return flux + .doOnSubscribe((sub) -> debug(">>> "+msg)) + .doOnComplete(() -> { + debug("<<< "+msg+" OK"); + }) + .doOnCancel(() -> { + debug("<<< "+msg+" CANCEL"); + }) + .doOnError((error) -> { + debug("<<< "+msg+" ERROR: "+ExceptionUtil.getMessage(error)); + }); + } else { + return flux; + } + } + + private Mono log(String msg, Mono mono) { + if (DEBUG) { + return mono + .doOnSubscribe((sub) -> debug(">>> "+msg)) + .doOnCancel(() -> debug("<<< "+msg+" CANCEL")) + .doOnSuccess((data) -> { + debug("<<< "+msg+" OK"); + }) + .doOnError((error) -> { + debug("<<< "+msg+" ERROR: "+ExceptionUtil.getMessage(error)); + }); + } else { + return mono; + } + } + + private Mono client_getInfo() { + return log("client.info().get()", + _client.info().get(GetInfoRequest.builder().build()) + ); + } + + private Mono operations_getOrgId() { + return log("operations.organizations.get(name="+params.getOrgName()+")", + _operations.organizations().get(OrganizationInfoRequest.builder() + .name(params.getOrgName()) + .build() + ) + .map(OrganizationDetail::getId) + ); + } + + private Mono> operations_listApps() { + return log("operations.applications.list()", + _operations.applications() + .list() + .map((appSummary) -> + CFWrappingV2.wrap(appSummary, getApplicationExtras(appSummary.getName())) + ) + .collectList() + .map(ImmutableList::copyOf) + ); + } + + private Mono client_getApplication(UUID appId) { + return log("client.applicationsV2.get(id="+appId+")", + _client.applicationsV2() + .get(org.cloudfoundry.client.v2.applications.GetApplicationRequest.builder() + .applicationId(appId.toString()) + .build() + ) + ); + } + + private Mono client_getStack(String stackId) { + return log("client.stacks.get(id="+stackId+")", + _client.stacks().get(GetStackRequest.builder() + .stackId(stackId) + .build() + ) + ); + } + + private Mono client_setHealthCheck(UUID guid, String hcType) { + return log("client.applicationsV2.update(id="+guid+", hcType="+hcType+")", + _client.applicationsV2() + .update(UpdateApplicationRequest.builder() + .applicationId(guid.toString()) + .healthCheckType(hcType) + .build() + ) + ); + } + + private Mono client_listBuildpacks(Integer page) { + return log("client.buildpacks.list(page="+page+")", + _client.buildpacks() + .list(ListBuildpacksRequest.builder() + .page(page) + .build() + ) + ); + } + + private Mono client_setEnv(UUID appId, Map environment) { + return log("client.applicationsV2.update(id="+appId+", env=...)", + _client.applicationsV2() + .update(UpdateApplicationRequest.builder() + .applicationId(appId.toString()) + .environmentJsons(environment) + .build()) + .then() + ); + } + + private Mono client_deleteServiceInstance(ServiceInstance s) { + return log("client.serviceInstances.delete(id="+s.getId()+")", + _client.serviceInstances().delete(DeleteServiceInstanceRequest.builder() + .serviceInstanceId(s.getId()) + .build() + ) + .then() + ); + } + + private Mono client_deleteUserProvidedService(ServiceInstance s) { + return log("client.userProvidedServiceInstances.delete(id="+s.getId()+")", + _client.userProvidedServiceInstances().delete(DeleteUserProvidedServiceInstanceRequest.builder() + .userProvidedServiceInstanceId(s.getId()) + .build() + ) + ); + } + + @Override + public Mono getUserName() { + return log("uaa.getUsername", + /* + * UAA times out at start until the client is cached. Needs ~2 mins to fetch the user. + * Instead get userID from client info and then fetch the user complete data from id to get the user name. This works and bypasses UAA + */ +// _uaa.getUsername() + info + .flatMap(info -> _client + .users() + .get(GetUserRequest.builder().userId(info.getUser()).build()) + .map(response -> response.getEntity().getUsername())) + ).timeout(GET_USERNAME_TIMEOUT); + } + + @Override + public String getRefreshToken() { + return refreshToken; + } + + @Override + public Flux getRefreshTokens() { + return refreshTokensFlux; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/DefaultCloudFoundryClientFactoryV2.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/DefaultCloudFoundryClientFactoryV2.java new file mode 100644 index 000000000..f288f9631 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/DefaultCloudFoundryClientFactoryV2.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.client.v2; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFClientParams; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.CloudFoundryClientFactory; + +public class DefaultCloudFoundryClientFactoryV2 extends CloudFoundryClientFactory { + + public static final DefaultCloudFoundryClientFactoryV2 INSTANCE = new DefaultCloudFoundryClientFactoryV2(); + + /** + * Use 'INSTANCE' constant instead. This class is a singleton. + */ + private DefaultCloudFoundryClientFactoryV2() {} + + private CloudFoundryClientCache clientFactory = new CloudFoundryClientCache(); + + @Override + public ClientRequests getClient(CFClientParams params) { + return new DefaultClientRequestsV2(clientFactory, params); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/ReactorUtils.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/ReactorUtils.java new file mode 100644 index 000000000..1a25e4dd0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/client/v2/ReactorUtils.java @@ -0,0 +1,289 @@ +/******************************************************************************* + * Copyright (c) 2016, 2018 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.cf.client.v2; + +import java.io.IOException; +import java.time.Duration; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import org.eclipse.core.runtime.OperationCanceledException; +import org.reactivestreams.Publisher; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +/** + * @author Kris De Volder + */ +public class ReactorUtils { + + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(45); // reflects default timeout of Mono.block in reactor 2.x. + public static boolean DUMP_STACK_ON_TIMEOUT = false; + + /** + * Convert a {@link CancelationToken} into a Mono that raises + * an {@link OperationCanceledException} when the token is canceled. + */ + public static Mono toMono(CancelationToken cancelToken) { + return Mono.delay(Duration.ofSeconds(1)) + .flatMap((ping) -> + cancelToken.isCanceled() + ? Mono.error(new OperationCanceledException()) + : Mono.empty() + ) + .repeatWhenEmpty((x) -> x); + } + + /** + * Similar to Mono.get but logs a more traceable version of the exception to Eclipse's error + * log before 'rethrowing' it. + *

+ * This is useful because the actual exception is pretty hard to trace. It doesn't even 'point' + * to the line where 'get' was called. + */ + public static T get(Mono mono) throws Exception { + try { + return mono.block(DEFAULT_TIMEOUT); + } catch (Exception e) { + dumpStacks(); + throw new IOException(e); + } + } + + /** + * Similar to Mono.get but logs a more traceable version of the exception to Eclipse's error + * log before 'rethrowing' it. + *

+ * This is useful because the actual exception is pretty hard to trace. It doesn't even 'point' + * to the line where 'get' was called. + */ + public static T get(Duration timeout, CancelationToken cancelationToken, Mono mono) throws Exception { + try { + return Mono.first(mono, toMono(cancelationToken)) + .onErrorResume(errorFilter(cancelationToken)) + .block(timeout); + } catch (Exception e) { + //dumpStacks(); + throw new IOException(e); + } + } + + + public static List get(Duration t, Mono> m) throws IOException { + try { + return m.block(t); + } catch (Exception e) { + dumpStacks(); + throw new IOException(e); + } + } + + private static void dumpStacks() { + if (DUMP_STACK_ON_TIMEOUT) { + System.out.println(getStackDumps().toString()); + } + } + + /** + * A 'filter' to use as a Mono.otherwise hanlder. It transforms any exception into {@link OperationCanceledException} + * when cancelationToken has been canceled. + */ + private static Function> errorFilter(CancelationToken cancelationToken) { + return (Throwable e) -> cancelationToken.isCanceled()?Mono.error(new OperationCanceledException()):Mono.error(e); + } + + /** + * Deprecated because this is really the same as Mono.justOrEmpty, so use that instead. + */ + @Deprecated + public static Mono just(T it) { + return it == null ? Mono.empty() : Mono.just(it); + } + + /** + * @return A function that can be passed to Mono.otherwise to convert a specific exception type into + * Mono.empty(). + */ + public static Function> suppressException(Class exceptionType) { + return (Throwable caught) -> { + if (exceptionType.isAssignableFrom(caught.getClass())) { + return Mono.empty(); + } else { + return Mono.error(caught); + } + }; + } + + /** + * Build a Mono that executes a given number of Mono one after the + * other. + */ + @SafeVarargs + public static Mono sequence(Mono... tasks) { + Mono seq = Mono.empty(); + for (Mono t : tasks) { + seq = seq.then(t); + } + return seq; + } + + /** + * Execute a bunch of mono in parallel. All monos are executed to completion (rather than canceled early + * when one of them fails) + *

+ * When at least one operation has failed then, upon completion or failure of the last Mono we guarantee that at least + * one of the exceptions is propagated. + */ + public static Mono safeMerge(Flux> operations, int concurrency) { + AtomicReference failure = new AtomicReference<>(null); + return Flux.merge( + operations + .map((Mono op) -> { + return op.onErrorResume((e) -> { + failure.compareAndSet(null, e); + return Mono.empty(); + }); + }), + concurrency //limit concurrency otherwise troubles (flooding/choking request broker?) + ) + .then(Mono.defer(() -> { + Throwable error = failure.get(); + if (error!=null) { + return Mono.error(error); + } else { + return Mono.empty(); + } + })); + } + + /** + * Attach a timestamp to each element in a Stream + */ + public static Flux> timestamp(Flux stream) { + return stream.map((e) -> Tuples.of(e, System.currentTimeMillis())); + } + + /** + * Sorts the elements in a flux in a moving time window. I.e. this assumes element order may be + * scrambled but the scrambling has a certain 'time localilty' to it. So we only need to consider + * sorting of elements that arrive 'close to eachother'. + *

+ * WARNING: The returned flux is intended for a single subscriber. It only maintains a + * single buffer for sorting stream elements. This buffer is consumed when elements + * are released to any subscriber. Therefore if one subscriber received a element it is gone + * from the buffer and will not be delivered to the other subscribers. + * + * @param stream The stream to be sorted + * @param comparator Compare function to sort with + * @param bufferTime The 'window' of time beyond which we don't need to compare elements. + */ + public static Flux sort(Flux stream, Comparator comparator, Duration bufferTime) { + + class SorterAccumulator { + + final PriorityQueue> holdingPen = new PriorityQueue<>((Tuple2 o1, Tuple2 o2) -> { + return comparator.compare(o1.getT1(), o2.getT1()); + }); + + final Flux released = Flux.fromIterable(() -> new Iterator() { + @Override + public boolean hasNext() { + Tuple2 nxt; + synchronized (holdingPen) { + nxt = holdingPen.peek(); + } + return nxt!=null && isOldEnough(nxt); + } + + private boolean isOldEnough(Tuple2 nxt) { + long age = System.currentTimeMillis() - nxt.getT2(); + return age > bufferTime.toMillis(); + } + + @Override + public T next() { + synchronized (holdingPen) { + return holdingPen.remove().getT1(); + } + } + }); + + public SorterAccumulator next(Flux> window) { + window.subscribe((e) -> { + synchronized (holdingPen) { + holdingPen.add(e); + } + }); + return this; + } + + public Flux getReleased() { + return released; + } + + public Publisher drain() { + return Flux.fromIterable(holdingPen) + .map(Tuple2::getT1); + } + } + + SorterAccumulator sorter = new SorterAccumulator(); + return timestamp(stream) + .window(bufferTime) + .scan(sorter, SorterAccumulator::next) + .concatMap(SorterAccumulator::getReleased) + .concatWith(sorter.drain()); + } + + protected static StringBuffer getStackDumps() { + StringBuffer sb = new StringBuffer(); + Map traces = Thread.getAllStackTraces(); + for (Map.Entry entry : traces.entrySet()) { + sb.append(entry.getKey().toString()); + sb.append("\n"); + for (StackTraceElement element : entry.getValue()) { + sb.append(" "); + sb.append(element.toString()); + sb.append("\n"); + } + sb.append("\n"); + } + return sb; + } + + + /** + * Connect a mono to a CompletableFuture so that the result of the mono + * can be retrieved from the {@link CompletableFuture} by calling it's 'get' + * method. + */ + public static void completeWith(CompletableFuture future, Mono mono) { + mono.doOnNext((T v) -> { + future.complete(v); + }) + .doOnError((Throwable e) -> { + future.completeExceptionally(e); + }) + .subscribeOn(Schedulers.elastic()) + .subscribe(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/DebugStrategyManager.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/DebugStrategyManager.java new file mode 100644 index 000000000..ac8afb9c4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/DebugStrategyManager.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.debug; + +import java.util.List; + +import org.eclipse.core.runtime.Assert; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +/** + * This class is responsible for managing the debug strategies. It is also responsible for managing the life + * cycle of process listeners abd other {@link Disposable}s that may be associated with a debug strategy and + * should be disposed when the manager itself is disposed. + *

+ * + * @author Kris De Volder + */ +public class DebugStrategyManager implements Disposable { + + + private DebugSupport strategy; + private Disposable processTracker; + + public DebugStrategyManager(List list, BootDashViewModel viewModel) { + // At the moment there's only one supported debug strategy, in the future there may be more than one. + // In that case this class may need be generalized to manage a collection of strategies somehow. + Assert.isLegal(list.size()==1); + this.strategy = list.get(0); + this.processTracker = strategy.createProcessTracker(viewModel); + } + + @Override + public void dispose() { + if (this.processTracker!=null) { + this.processTracker.dispose(); + this.processTracker = null; + } + } + + public DebugSupport getStrategy() { + return strategy; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/DebugSupport.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/DebugSupport.java new file mode 100644 index 000000000..7a0e60ce8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/DebugSupport.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.debug; + +import java.util.Map; + +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.debug.core.model.IProcess; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.ops.Operation; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; +import org.springframework.ide.eclipse.boot.util.ProcessListenerAdapter; +import org.springframework.ide.eclipse.boot.util.ProcessTracker; + +/** + * Abstract class that must be implemented to add debug support to a CF application. + * + * @author Kris De Volder + */ +public abstract class DebugSupport { + + /** + * Determine whether debugging can be supported (using the strategy impemented by this DebugSupport instance) + */ + public abstract boolean isSupported(CloudAppDashElement app); + /** + * If isSupported returns false than the support strategy may also return an explanation why the strategy is not + * supported (e.g. PCF version too old, SSH support disabled etc.) + */ + public abstract String getNotSupportedMessage(CloudAppDashElement app); + + /** + * Creates operation that does whatever is needed to get debugger connected to the targetted app. + */ + public abstract Operation createOperation(CloudAppDashElement app, String opName, UserInteractions ui, CancelationToken cancelToken); + + /** + * Called to allow debug support to muck around with environment variables so that it can + * do things like add debugging options to 'JAVA_OPTS'. + */ + public abstract void setupEnvVars(Map environmentVariables); + + /** + * Like setupEnvVars, but called when debugging is disabled. The debug strategy should try to + * undo any changes it made to the env vars to enable debugging. + */ + public abstract void clearEnvVars(Map environmentVariables); + + /** + * Determines whether debugger is currently attached to the targetted app. + */ + public abstract boolean isDebuggerAttached(CloudAppDashElement app); + + /** + * A debug strategy typically involves creating some type of launch that establishes + * a debug connection. To be able to update the debug state in response to changes in + * launches, its necessary to be able to determine what dashboard element corresponds to + * a given launch. Therefore a debug strategy must implement this method. + * + * @return The corresponding CDE for a given launch, or null + */ + public abstract CloudAppDashElement getElementFor(ILaunch l, BootDashViewModel viewModel); + + /** + * Provides a process tracker. Subclasses may override if the default implementation is not suitable. + *

+ * The process tracker is responsible to listen for changes in the Eclipse debug ui so it can make + * bootdash elements 'debug' state update when processes and/or debug targets are created or terminated. + */ + public ProcessTracker createProcessTracker(final BootDashViewModel viewModel) { + return new ProcessTracker(new ProcessListenerAdapter() { + @Override + public void debugTargetCreated(ProcessTracker tracker, IDebugTarget target) { + handleStateChange(target.getLaunch(), "debugTargetCreated"); + } + @Override + public void debugTargetTerminated(ProcessTracker tracker, IDebugTarget target) { + handleStateChange(target.getLaunch(), "debugTargetTerminated"); + } + + @Override + public void processTerminated(ProcessTracker tracker, IProcess process) { + //Typically a debug strategy only needs to care about debugtargets, not IProcesses. + // So nothing to do here. + } + @Override + public void processCreated(ProcessTracker tracker, IProcess process) { + //Typically a debug strategy only needs to care about debugtargets, not IProcesses. + // So nothing to do here. + } + private void handleStateChange(ILaunch l, Object info) { + CloudAppDashElement e = getElementFor(l, viewModel); + if (e!=null) { + BootDashModel model = e.getBootDashModel(); + model.notifyElementChanged(e, info); + } + } + }); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SelectRunTargetLaunchTabModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SelectRunTargetLaunchTabModel.java new file mode 100644 index 000000000..da3459271 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SelectRunTargetLaunchTabModel.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.debug; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.launch.LaunchTabSelectionModel; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; + +import com.google.common.collect.ImmutableSet; + +public class SelectRunTargetLaunchTabModel extends LaunchTabSelectionModel { + +// public static SelectProjectLaunchTabModel create() { +// LiveVariable project = new LiveVariable(); +// ExistingBootProjectSelectionValidator validator = new ExistingBootProjectSelectionValidator(project); +// return new SelectProjectLaunchTabModel(project, validator); +// } + + private BootDashViewModel context; + + public SelectRunTargetLaunchTabModel(LiveVariable variable, LiveExpression validator, BootDashViewModel context) { + super(variable, validator); + this.context = context; + } + + @Override + public void initializeFrom(ILaunchConfiguration conf) { + selection.setValue(SshDebugLaunchConfigurationDelegate.getRunTarget(conf, context)); + getDirtyState().setValue(false); + } + + @Override + public void performApply(ILaunchConfigurationWorkingCopy conf) { + SshDebugLaunchConfigurationDelegate.setRunTarget(conf, selection.getValue()); + getDirtyState().setValue(false); + } + + @Override + public void setDefaults(ILaunchConfigurationWorkingCopy conf) { +// if (choices.length>0) { +// SshDebugLaunchConfigurationDelegate.setRunTarget(conf, choices[0]); +// } + } + + public CloudFoundryRunTarget[] getChoices() { + ImmutableSet targets = context.getRunTargets().getValues(); + ArrayList interesting = new ArrayList<>(targets.size()); + for (RunTarget t : targets) { + if (t instanceof CloudFoundryRunTarget) { + interesting.add((CloudFoundryRunTarget) t); + } + } + return interesting.toArray(new CloudFoundryRunTarget[interesting.size()]); + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SelectRunTargetLaunchTabSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SelectRunTargetLaunchTabSection.java new file mode 100644 index 000000000..064a08ac5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SelectRunTargetLaunchTabSection.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.debug; + +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.util.NameableLabelProvider; +import org.springframework.ide.eclipse.boot.launch.util.DelegatingLaunchConfigurationTabSection; +import org.springframework.ide.eclipse.boot.launch.util.ILaunchConfigurationTabSection; +import org.springsource.ide.eclipse.commons.livexp.ui.ChooseOneSectionCombo; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; + +/** + * @author Kris De Volder + */ +public class SelectRunTargetLaunchTabSection { + + public static ILaunchConfigurationTabSection create(IPageWithSections owner, SelectRunTargetLaunchTabModel model) { + ChooseOneSectionCombo ui = + new ChooseOneSectionCombo(owner, "Target", model, model.getChoices()); + ui.setLabelProvider(new NameableLabelProvider()); + return new DelegatingLaunchConfigurationTabSection(owner, model, ui); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugLaunchConfigurationDelegate.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugLaunchConfigurationDelegate.java new file mode 100644 index 000000000..41f97dc52 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugLaunchConfigurationDelegate.java @@ -0,0 +1,386 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.debug; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.debug.core.model.IProcess; +import org.eclipse.jdt.internal.launching.LaunchingMessages; +import org.eclipse.jdt.internal.launching.LaunchingPlugin; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jdt.launching.IVMConnector; +import org.eclipse.jdt.launching.JavaRuntime; +import org.eclipse.osgi.util.NLS; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.client.SshClientSupport; +import org.springframework.ide.eclipse.boot.dash.cf.client.SshHost; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.util.WaitFor; +import org.springframework.ide.eclipse.boot.util.ProcessListenerAdapter; +import org.springframework.ide.eclipse.boot.util.ProcessTracker; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +@SuppressWarnings("restriction") +public class SshDebugLaunchConfigurationDelegate extends AbstractBootLaunchConfigurationDelegate { + + public static final String TYPE_ID = "org.springframework.ide.eclipse.boot.dash.ssh.tunnel.launch"; + + private static final long DEBUG_CONNECT_TIMEOUT = 20000; + public static final String RUN_TARGET = "ssh.debug.runtarget.id"; + public static final String APP_NAME = "ssh.debug.app.name"; + public static final String INSTANCE_IDX = "ssh.debug.app.instance"; + + private static BootDashViewModel getContext() { + //TODO: it may be necessary to allow injecting this, for example via setting a threadlocal, + // to make code more amenable to unit testing. + //This method is here because LaunchConf delegates are created by eclipse debug framework and + // so we can't easily inject a context object into it. + //The only method that should be calling this is 'launch'. Everything else should be doing + // the proper thing and pass in the model as parameter somehow. + return BootDashActivator.getDefault().getModel(); + } + + @Override + public void launch(ILaunchConfiguration conf, String mode, ILaunch launch, IProgressMonitor mon) + throws CoreException { + conf = configureClassPathProviders(conf); + Assert.isTrue(ILaunchManager.DEBUG_MODE.equals(mode)); + BootDashViewModel context = getContext(); + mon.beginTask("Establish SSH Debug Connection to "+getAppName(conf)+" on "+getRunTarget(conf, context), 4); + CloudAppDashElement app = null; + try { + CloudFoundryRunTarget target = getRunTarget(conf, context); + SshDebugSupport debugSupport = getDebugSupport(conf, context); + + app = getApp(conf, context); + + if (app!=null && target!=null && debugSupport.isSupported(app)) { + //1: determine SSH tunnel parameters + app.log("Fetching SSH tunnel parameters..."); + SshClientSupport sshInfo = target.getSshClientSupport(); + SshHost sshHost = sshInfo.getSshHost(); + String sshUser = sshInfo.getSshUser(app.getAppGuid(), getInstanceIndex(conf)); + String sshCode = sshInfo.getSshCode(); + int remotePort = debugSupport.getRemotePort(); + + app.log("SSH tunnel parameters:"); + app.log(" host: "+sshHost); + app.log(" user: "+sshUser); + app.log(" code: "+sshCode); + app.log(" remote port: "+remotePort); + mon.worked(1); + + //2: create tunnel + app.log("Creating tunnel..."); + SshTunnel tunnel = new SshTunnelImpl(sshHost, sshUser, sshCode, remotePort, app); //TODO: use SshTunnelFactory? + + //3: connect debugger stuff + app.log("Launching remote debug connector..."); + launchRemote(tunnel, conf, launch, new SubProgressMonitor(mon, 1)); + app.log("Launching remote debug connector... DONE"); + } + } catch (Exception e) { + if (app!=null) { + app.log("ERROR: "+ExceptionUtil.getMessage(e)); + } + throw ExceptionUtil.coreException(e); + } finally { + mon.done(); + } + } + + public static int getInstanceIndex(ILaunchConfiguration conf) { + try { + return conf.getAttribute(INSTANCE_IDX, 0); + } catch (Exception e) { + BootDashActivator.log(e); + } + return 0; + } + + private SshDebugSupport getDebugSupport(ILaunchConfiguration conf, BootDashViewModel context) { + CloudAppDashElement app = getApp(conf, context); + if (app!=null) { + DebugSupport ds = app.getDebugSupport(); + if (ds instanceof DebugSupport) { + return (SshDebugSupport) ds; + } + } + return null; + } + + + public static String getAppName(ILaunchConfiguration conf) { + return getString(conf, APP_NAME); + } + + public static CloudAppDashElement getApp(ILaunchConfiguration conf, BootDashViewModel context) { + String appName = getAppName(conf); + if (appName!=null) { + BootDashModel section = context.getSectionByTargetId(getRunTargetId(conf)); + if (section instanceof CloudFoundryBootDashModel) { + CloudFoundryBootDashModel cfmodel = (CloudFoundryBootDashModel) section; + return cfmodel.getApplication(appName); + } + } + return null; + } + + public static void setAppName(ILaunchConfigurationWorkingCopy conf, String name) { + if (name!=null) { + conf.setAttribute(APP_NAME, name); + } else { + conf.removeAttribute(APP_NAME); + } + } + + public static void setRunTarget(ILaunchConfigurationWorkingCopy conf, CloudFoundryRunTarget target) { + if (target!=null) { + conf.setAttribute(RUN_TARGET, target.getId()); + } else { + conf.removeAttribute(RUN_TARGET); + } + } + + private static String getRunTargetId(ILaunchConfiguration conf) { + String at = RUN_TARGET; + return getString(conf, at); + } + + + protected static String getString(ILaunchConfiguration conf, String attName) { + try { + return conf.getAttribute(attName, (String)null); + } catch (CoreException e) { + BootDashActivator.log(e); + } + return null; + } + + public static CloudFoundryRunTarget getRunTarget(ILaunchConfiguration conf, BootDashViewModel context) { + try { + String id = conf.getAttribute(RUN_TARGET, (String)null); + if (id!=null) { + RunTarget target = context.getRunTargetById(id); + if (target instanceof CloudFoundryRunTarget) { + return (CloudFoundryRunTarget) target; + } + } + } catch (Exception e) { + BootDashActivator.log(e); + } + return null; + } + + /** + * Create debugging target similar to a remote debugging session would and add them to the launch. + * This is to support debugging of the remote boot-app that is reachable over http tunnel + * the client creates. From our side this just as if we are opening a remote debug + * session to the client. + */ + private void launchRemote(final SshTunnel tunnel, ILaunchConfiguration configuration, final ILaunch launch, IProgressMonitor _monitor) throws CoreException { + int port = tunnel.getLocalPort(); + final IProgressMonitor monitor = _monitor==null?new NullProgressMonitor():_monitor; + + monitor.beginTask(NLS.bind(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Attaching_to__0_____1, new String[]{configuration.getName()}), 3); + // check for cancellation + if (monitor.isCanceled()) { + return; + } + try { + monitor.subTask(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Verifying_launch_attributes____1); + + //String connectorId = "org.eclipse.jdt.launching.socketListenConnector";//getVMConnectorId(configuration); + String connectorId = "org.eclipse.jdt.launching.socketAttachConnector"; + final IVMConnector connector = JavaRuntime.getVMConnector(connectorId); + if (connector == null) { + abort(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Connector_not_specified_2, null, IJavaLaunchConfigurationConstants.ERR_CONNECTOR_NOT_AVAILABLE); + } + + final Map argMap = new HashMap<>(); + + int connectTimeout = Platform.getPreferencesService().getInt( + LaunchingPlugin.ID_PLUGIN, + JavaRuntime.PREF_CONNECT_TIMEOUT, + JavaRuntime.DEF_CONNECT_TIMEOUT, + null); + argMap.put("hostname", "localhost"); + argMap.put("timeout", ""+connectTimeout); + argMap.put("port", ""+port); + + // check for cancellation + if (monitor.isCanceled()) { + return; + } + + monitor.worked(1); + + + monitor.subTask(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Creating_source_locator____2); + // set the default source locator if required + setDefaultSourceLocator(launch, configuration); + monitor.worked(1); + + // connect to remote VM + try { + new WaitFor(DEBUG_CONNECT_TIMEOUT) { + @Override + public void run() throws Exception { + connector.connect(argMap, monitor, launch); + } + }; + new ProcessTracker(new ProcessListenerAdapter() { + @Override + public void debugTargetTerminated(ProcessTracker tracker, IDebugTarget target) { + handleTermination(tracker, target.getLaunch()); + } + @Override + public void processTerminated(ProcessTracker tracker, IProcess process) { + handleTermination(tracker, process.getLaunch()); + } + private void handleTermination(ProcessTracker tracker, ILaunch targetLaunch) { + if (launch.equals(targetLaunch)) { + tracker.dispose(); + tunnel.dispose(); + } + } + }); + } catch (Exception e) { + terminateAllTargets(launch); + throw ExceptionUtil.coreException(e); + } + + // check for cancellation + if (monitor.isCanceled()) { + terminateAllTargets(launch); + return; + } + } + finally { + monitor.done(); + } + } + + public void terminateAllTargets(final ILaunch launch) { + //Note: its better to discconect debugtargets before terminating processes + // because that allows a cleaner disconnect from the debugged process. + // (If the devtools client process is terminated its no longer possible to talk to the + // debugged process). + IDebugTarget[] debugTargets = launch.getDebugTargets(); + for (int i = 0; i < debugTargets.length; i++) { + IDebugTarget target = debugTargets[i]; + if (target.canDisconnect()) { + try { + target.disconnect(); + } catch (Exception e) { + BootActivator.log(e); + } + } + } + IProcess[] processes = launch.getProcesses(); + for (IProcess process : processes) { + if (process.canTerminate()) { + try { + process.terminate(); + } catch (Exception e) { + BootActivator.log(e); + } + } + } + } + + + public static ILaunchConfiguration getOrCreateLaunchConfig(CloudAppDashElement app) throws CoreException { + IProject project = app.getProject(); + String appName = app.getName(); + CloudFoundryRunTarget target = app.getTarget(); + Assert.isTrue(project!=null); + ILaunchConfiguration existing = findConfig(project, target, appName); + ILaunchConfigurationWorkingCopy wc; + if (existing!=null) { + return existing; + } else { + wc = createConfiguration(project, target, appName); + return wc.doSave(); + } + } + + private static ILaunchConfigurationWorkingCopy createConfiguration(IProject project, CloudFoundryRunTarget target, String appName) throws CoreException { + ILaunchConfigurationType configType = getLaunchType(); + ILaunchConfigurationWorkingCopy wc = configType.newInstance(null, getLaunchMan().generateLaunchConfigurationName("ssh-tunnel["+appName+"]")); + BootLaunchConfigurationDelegate.setProject(wc, project); + setRunTarget(wc, target); + setAppName(wc, appName); + wc.setMappedResources(new IResource[] {project}); + return wc; + } + + public static ILaunchConfiguration findConfig(CloudAppDashElement app) { + IProject project = app.getProject(); + String appName = app.getName(); + CloudFoundryRunTarget target = app.getTarget(); + return findConfig(project, target, appName); + } + + private static ILaunchConfiguration findConfig(IProject project, CloudFoundryRunTarget target, String appName) { + try { + if (project!=null) { + for (ILaunchConfiguration c : getLaunchMan().getLaunchConfigurations(getLaunchType())) { + if ( + project.equals(BootLaunchConfigurationDelegate.getProject(c)) && + target.getId().equals(getRunTargetId(c)) && + appName.equals(getAppName(c)) + ) { + return c; + } + } + } + } catch (CoreException e) { + BootActivator.log(e); + } + return null; + } + + public static ILaunchConfigurationType getLaunchType() { + return getLaunchMan().getLaunchConfigurationType(TYPE_ID); + } + + + public static void doLaunch(CloudAppDashElement app, IProgressMonitor monitor) throws CoreException { + ILaunchConfiguration conf = SshDebugLaunchConfigurationDelegate.getOrCreateLaunchConfig(app); + conf.launch(ILaunchManager.DEBUG_MODE, monitor); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugLaunchConfigurationTabGroup.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugLaunchConfigurationTabGroup.java new file mode 100644 index 000000000..b81ea213f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugLaunchConfigurationTabGroup.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.debug; + +import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup; +import org.eclipse.debug.ui.CommonTab; +import org.eclipse.debug.ui.ILaunchConfigurationDialog; +import org.eclipse.debug.ui.ILaunchConfigurationTab; +import org.eclipse.debug.ui.ILaunchConfigurationTabGroup; +import org.eclipse.debug.ui.sourcelookup.SourceLookupTab; + +public class SshDebugLaunchConfigurationTabGroup extends AbstractLaunchConfigurationTabGroup { + + /** + * @see ILaunchConfigurationTabGroup#createTabs(ILaunchConfigurationDialog, String) + */ + @Override + public void createTabs(ILaunchConfigurationDialog dialog, String mode) { + ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] { + new SshDebugMainTab(), + //new JavaArgumentsTab(), + //new JavaJRETab(), + //new JavaClasspathTab(), + new SourceLookupTab(), + //new EnvironmentTab(), + new CommonTab() + }; + setTabs(tabs); + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugLaunchUIModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugLaunchUIModel.java new file mode 100644 index 000000000..3b297306f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugLaunchUIModel.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.debug; + +import org.eclipse.core.resources.IProject; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.launch.ExistingBootProjectSelectionValidator; +import org.springframework.ide.eclipse.boot.launch.SelectProjectLaunchTabModel; +import org.springframework.ide.eclipse.boot.launch.devtools.DevtoolsEnabledValidator; +import org.springframework.ide.eclipse.boot.launch.devtools.StringFieldLaunchTabModel; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.core.CompositeValidator; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.StringFieldModel; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; + +/** + * Model for the 'main type' selection widgetry on a launchconfiguration tab. + *

+ * Contains the 'logic' for the UI except for the widgets themselves. + * Can be unit tested without having to instantiate launch configuration dialogs + * etc. + * + * @author Kris De Volder + */ +public class SshDebugLaunchUIModel { + + public final SelectProjectLaunchTabModel project; + public final SelectRunTargetLaunchTabModel cfTarget; + public final StringFieldLaunchTabModel appName; + private BootDashViewModel context; + + public SshDebugLaunchUIModel(BootDashViewModel context) { + this.context = context; + project = createProjectSelectionModel(); + cfTarget = createCfTargetModel(context); + appName = createAppNameModel(); + } + + private SelectRunTargetLaunchTabModel createCfTargetModel(BootDashViewModel context) { + LiveVariable variable = new LiveVariable<>(); + LiveExpression validator = Validator.notNull(variable, "No Target selected"); + return new SelectRunTargetLaunchTabModel(variable, validator, context); + } + + private StringFieldLaunchTabModel createAppNameModel() { + StringFieldModel field = new StringFieldModel("App Name", ""); + field.validator(appNameValidator(project.selection, cfTarget.selection, field.getVariable())); + return new StringFieldLaunchTabModel(field, SshDebugLaunchConfigurationDelegate.APP_NAME); + } + + + private LiveExpression appNameValidator( + final LiveExpression projectSelection, + final LiveExpression targetSelection, + final LiveExpression appNameSelection + ) { + return new Validator() { + { + dependsOn(projectSelection); + dependsOn(appNameSelection); + dependsOn(targetSelection); + } + protected ValidationResult compute() { + CloudFoundryRunTarget target = targetSelection.getValue(); + if (target!=null) { + String name = appNameSelection.getValue(); + if (!StringUtil.hasText(name)) { + return ValidationResult.error("App Name not set"); + } + BootDashModel section = context.getSectionByTargetId(target.getId()); + if (section instanceof CloudFoundryBootDashModel) { + CloudFoundryBootDashModel cfSection = (CloudFoundryBootDashModel) section; + CloudAppDashElement app = cfSection.getApplication(name); + if (app==null) { + return ValidationResult.error("There is no app '"+name+"' in '"+target+"'"); + } + IProject project = projectSelection.getValue(); + IProject appProject = app.getProject(); + if (appProject==null) { + // we don't know what project goes with selected app so give the user + // the benefit of the doubdt + // => no error! + } else if (project==null) { + // not this validator's problem. (project validator should check this) + // => no error + } else { + if (!project.equals(appProject)) { + return ValidationResult.error("The app '"+name+"' is bound to a different project ("+appProject.getName()+")"); + } + } + } + } + return ValidationResult.OK; + } + }; + } + + private SelectProjectLaunchTabModel createProjectSelectionModel() { + LiveVariable project = new LiveVariable<>(); + CompositeValidator validator = new CompositeValidator(); + validator.addChild(new ExistingBootProjectSelectionValidator(project)); + validator.addChild(new DevtoolsEnabledValidator(project)); + return new SelectProjectLaunchTabModel(project, validator); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugMainTab.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugMainTab.java new file mode 100644 index 000000000..75e6329de --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugMainTab.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.debug; + +import static org.springframework.ide.eclipse.boot.ui.BootUIImages.BOOT_DEVTOOLS_ICON; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.launch.SelectProjectLaunchTabSection; +import org.springframework.ide.eclipse.boot.launch.devtools.StringFieldLaunchTabSection; +import org.springframework.ide.eclipse.boot.launch.util.LaunchConfigurationTabWithSections; +import org.springframework.ide.eclipse.boot.ui.BootUIImages; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; + +/** + * @author Kris De Volder + */ +public class SshDebugMainTab extends LaunchConfigurationTabWithSections implements IPageWithSections { + + @Override + public String getName() { + return "SSH Tunnel"; + } + + @Override + public Image getImage() { + return BootUIImages.getImage(BOOT_DEVTOOLS_ICON); + } + + @Override + protected List createSections() { + SshDebugLaunchUIModel model = new SshDebugLaunchUIModel(BootDashActivator.getDefault().getModel()); + return Arrays.asList(new IPageSection[] { + SelectProjectLaunchTabSection.create(this, model.project), + SelectRunTargetLaunchTabSection.create(this, model.cfTarget), + StringFieldLaunchTabSection.create(this, model.appName) + }); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugStartOperation.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugStartOperation.java new file mode 100644 index 000000000..931efdfc6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugStartOperation.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.debug; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.ops.CloudApplicationOperation; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; + +/** + * @author Kris De Volder + */ +public class SshDebugStartOperation extends CloudApplicationOperation { + + private CloudAppDashElement app; + + public SshDebugStartOperation(CloudAppDashElement app, DebugSupport debugSupport, CancelationToken cancelationToken) { + super("Starting SSH debugging for app '"+app.getName()+"'", app.getBootDashModel(), app.getName(), cancelationToken); + this.app = app; + } + + @Override + protected void doCloudOp(IProgressMonitor monitor) throws Exception, OperationCanceledException { + //TODO: wireup a progress monitor to pass to 'doLaunch' so its aware of cancelation token + SshDebugLaunchConfigurationDelegate.doLaunch(app, monitor); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugSupport.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugSupport.java new file mode 100644 index 000000000..2658bf38d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshDebugSupport.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.debug; + +import static org.springframework.ide.eclipse.boot.dash.cf.debug.SshDebugLaunchConfigurationDelegate.getApp; +import static org.springframework.ide.eclipse.boot.dash.cf.debug.SshDebugLaunchConfigurationDelegate.getLaunchType; + +import java.util.Map; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.model.IDebugTarget; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.ops.Operation; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; +import org.springframework.ide.eclipse.boot.launch.util.BootLaunchUtils; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +/** + * Uses ssh tunnelling on Diego to support debugging of app running on CF. + * + * @author Kris De Volder + */ +public class SshDebugSupport extends DebugSupport { + + public static final SshDebugSupport INSTANCE = new SshDebugSupport(); + + private static final int REMOTE_DEBUG_PORT = 47822; + private static final String REMOTE_DEBUG_JVM_ARGS = "-Xdebug -Xrunjdwp:server=y,transport=dt_socket,suspend=n,address="+REMOTE_DEBUG_PORT; + private static final String JAVA_OPTS = "JAVA_OPTS"; + + private SshDebugSupport() {} + + @Override + public boolean isSupported(CloudAppDashElement app) { + String notSupportedMessage = getNotSupportedMessage(app); + return notSupportedMessage==null; + } + + @Override + public String getNotSupportedMessage(CloudAppDashElement app) { + //TODO: There are a number of different ways that ssh and/or diego might be disabled for + // an app. e.g. it can be disabled on the app itself, on the space, on the org or + // on the whole CF installation. This check should recognize these situation. + // At the moment it pretty much returns true if the CF has global info about the 'ssh-host', + // but this probably usually the case on any recent enough version of PCF, even if + // ssh has been explicitly disabled. + CloudFoundryRunTarget target = app.getTarget(); + try { + if (target.getSshClientSupport().getSshHost()==null) { + return "Cloud controller doesn't specify an ssh-host. This probably means your version of CloudFoundry doesn't support SSH."; + } + return null; + } catch (Exception e) { + BootDashActivator.log(e); //for traceability + String msg = ExceptionUtil.getMessage(e); + if (!StringUtil.hasText(msg)) { + msg = "Exception: "+e.getClass().getName(); + } + return msg; + } + } + + @Override + public boolean isDebuggerAttached(CloudAppDashElement app) { + ILaunchConfiguration conf = SshDebugLaunchConfigurationDelegate.findConfig(app); + if (conf!=null) { + for (ILaunch l : BootLaunchUtils.getLaunches(conf)) { + if (!l.isTerminated()) { + for (IDebugTarget dt : l.getDebugTargets()) { + if (!dt.isTerminated()) { + //Active debug target found, so debugger is attached. + return true; + } + } + return true; + } + } + } + return false; + } + + @Override + public Operation createOperation(CloudAppDashElement app, String opName, UserInteractions ui, CancelationToken cancelationToken) { + return new SshDebugStartOperation(app, this, cancelationToken); + } + + @Override + public void setupEnvVars(Map env) { + String javaOpts = clearJavaOpts(env.get(JAVA_OPTS)); + StringBuilder sb = new StringBuilder(javaOpts); + if (sb.length() > 0) { + sb.append(' '); + } + sb.append(REMOTE_DEBUG_JVM_ARGS); + env.put(JAVA_OPTS, sb.toString()); + + } + + private static String clearJavaOpts(String opts) { + if (opts!=null) { + opts = opts.replaceAll(REMOTE_DEBUG_JVM_ARGS + "\\s*", ""); + return opts; + } else { + return ""; + } + } + + + @Override + public void clearEnvVars(Map env) { + String jopts = clearJavaOpts(env.get(JAVA_OPTS)); + if (StringUtil.hasText(jopts)) { + env.put(JAVA_OPTS, clearJavaOpts(env.get(JAVA_OPTS))); + } else { + env.remove(JAVA_OPTS); + } + } + + public int getRemotePort() { + return REMOTE_DEBUG_PORT; + } + + @Override + public CloudAppDashElement getElementFor(ILaunch l, BootDashViewModel context) { + try { + ILaunchConfigurationType interestingType = getLaunchType(); + ILaunchConfiguration conf = l.getLaunchConfiguration(); + if (interestingType.equals(conf.getType())) { + return getApp(conf, context); + } + } catch (CoreException e) { + BootActivator.log(e); + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshTunnel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshTunnel.java new file mode 100644 index 000000000..dc02b4be7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshTunnel.java @@ -0,0 +1,12 @@ +package org.springframework.ide.eclipse.boot.dash.cf.debug; + +import org.springsource.ide.eclipse.commons.livexp.core.OnDispose; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +public interface SshTunnel extends Disposable, OnDispose { + + int getLocalPort(); + + boolean isDisposed(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshTunnelFactory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshTunnelFactory.java new file mode 100644 index 000000000..42b1ed206 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshTunnelFactory.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2018 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.cf.debug; + +import org.springframework.ide.eclipse.boot.dash.cf.client.SshHost; +import org.springframework.ide.eclipse.boot.dash.util.LogSink; + +@FunctionalInterface +public interface SshTunnelFactory { + + SshTunnel create( + SshHost sshHost, + String user, + String oneTimeCode, + int remotePort, + LogSink log, + int localPort + ) throws Exception; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshTunnelImpl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshTunnelImpl.java new file mode 100644 index 000000000..a346a6d99 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/debug/SshTunnelImpl.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.debug; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.SocketTimeoutException; + +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.springframework.ide.eclipse.boot.dash.cf.client.SshHost; +import org.springframework.ide.eclipse.boot.dash.util.LogSink; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +import net.schmizz.keepalive.KeepAliveProvider; +import net.schmizz.keepalive.KeepAliveRunner; +import net.schmizz.sshj.DefaultConfig; +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.connection.channel.direct.LocalPortForwarder; + +/** + * This class is responsible for creating an ssh tunnel to a remote port. This class implements + * Closeable, its close method must be called to close the tunnel and avoid resource leak. + */ +public class SshTunnelImpl extends AbstractDisposable implements SshTunnel { + + private boolean closeRequested = false; + private int localPort; + private SSHClient ssh; + private LocalPortForwarder portForwarder; + + public SshTunnelImpl(SshHost sshHost, String user, String oneTimeCode, int remotePort, LogSink log) throws Exception { + this(sshHost, user, oneTimeCode, remotePort, log, 0); + } + + public SshTunnelImpl(SshHost sshHost, String user, String oneTimeCode, int remotePort, LogSink log, int _localPort) throws Exception { + DefaultConfig config = new DefaultConfig(); + config.setKeepAliveProvider(KeepAliveProvider.KEEP_ALIVE); // Hopefuly this is better at detecting dropped connections from server (e.g. when app is stopped or dies). + ssh = new SSHClient(config); + ssh.addHostKeyVerifier(sshHost.getFingerPrint()); + log.log("Ssh client created"); + + ssh.connect(sshHost.getHost(), sshHost.getPort()); + ssh.authPassword(user, oneTimeCode); + KeepAliveRunner keepAlive = (KeepAliveRunner) ssh.getConnection().getKeepAlive(); + keepAlive.setKeepAliveInterval(5); + keepAlive.setMaxAliveCount(1); + log.log("Ssh client connected"); + + ServerSocket ss = new ServerSocket(_localPort); + ss.setSoTimeout(5_000); + localPort = ss.getLocalPort(); + Job job = new Job("SshTunnel port forwarding") { + + @Override + protected IStatus run(IProgressMonitor arg0) { + final LocalPortForwarder.Parameters params = new LocalPortForwarder.Parameters("0.0.0.0", localPort, "localhost", remotePort); + try { + portForwarder = ssh.newLocalPortForwarder(params, ss); + boolean retry; + do { + retry = false; + try { + portForwarder.listen(); + } catch (IOException e) { + if (!closeRequested) { + if (e instanceof SocketTimeoutException) { + //don't log it happens all the time and is expected + } else { + log.log(ExceptionUtil.getMessage(e)); + } + retry = true; + } + } + } while (retry); + } finally { + try { + ss.close(); + } catch (IOException e) { + } + } + return Status.OK_STATUS; + } + }; + job.setSystem(true); + job.schedule(); + log.log("Ssh tunnel created: localPort = "+localPort); + } + + @Override + synchronized public void dispose() { + if (portForwarder!=null) { + try { + portForwarder.close(); + } catch (Exception e) { + } + portForwarder = null; + } + if (ssh!=null) { + try { + ssh.disconnect(); + } catch (Exception e) { + } + ssh = null; + } + super.dispose(); + } + + public int getLocalPort() { + return localPort; + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameAnnotation.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameAnnotation.java new file mode 100644 index 000000000..ce99337aa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameAnnotation.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.deployment; + +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationPresentation; +import org.eclipse.jface.text.source.ImageUtilities; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.FontMetrics; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Canvas; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; + +/** + * Annotation for application name. Can be selected and unselected. Based on JDT + * projection annotation + * + * @author Alex Boyko + * + */ +public class AppNameAnnotation extends Annotation implements IAnnotationPresentation { + /** + * The type of CF application name annotations. + */ + public static final String TYPE = "cf.app.name"; //$NON-NLS-1$ + + private static final int COLOR= SWT.COLOR_GRAY; + + /** The state of this annotation */ + private boolean fIsSelected= false; + /** Indicates whether this annotation should be painted as range */ + private boolean fIsRangeIndication= false; + + /** + * Creates a new annotation. When isSelected + * is true the annotation is initially selected. + * + * @param isSelected true if the annotation should initially be selected, false otherwise + */ + public AppNameAnnotation(String text) { + super(TYPE, false, text); + } + + /** + * Creates a new annotation. + * + * @param text the app name + * @param selected selected or unselected + */ + public AppNameAnnotation(String text, boolean selected) { + this(text); + fIsSelected = selected; + } + + /** + * Enables and disables the range indication for this annotation. + * + * @param rangeIndication the enable state for the range indication + */ + public void setRangeIndication(boolean rangeIndication) { + fIsRangeIndication= rangeIndication; + } + + private void drawRangeIndication(GC gc, Canvas canvas, Rectangle r) { + final int MARGIN= 3; + + /* cap the height - at least on GTK, large numbers are converted to + * negatives at some point */ + int height= Math.min(r.y + r.height - MARGIN, canvas.getSize().y); + + gc.setForeground(canvas.getDisplay().getSystemColor(COLOR)); + gc.setLineWidth(0); // NOTE: 0 means width is 1 but with optimized performance + gc.drawLine(r.x + 4, r.y + 12, r.x + 4, height); + gc.drawLine(r.x + 4, height, r.x + r.width - MARGIN, height); + } + + /* + * @see org.eclipse.jface.text.source.IAnnotationPresentation#paint(org.eclipse.swt.graphics.GC, org.eclipse.swt.widgets.Canvas, org.eclipse.swt.graphics.Rectangle) + */ + public void paint(GC gc, Canvas canvas, Rectangle rectangle) { + Image image= getImage(); + if (image != null) { + ImageUtilities.drawImage(image, gc, canvas, rectangle, SWT.CENTER, SWT.TOP); + if (fIsRangeIndication) { + FontMetrics fontMetrics= gc.getFontMetrics(); + int delta= (fontMetrics.getHeight() - image.getBounds().height)/2; + rectangle.y += delta; + rectangle.height -= delta; + drawRangeIndication(gc, canvas, rectangle); + } + } + } + + /* + * @see org.eclipse.jface.text.source.IAnnotationPresentation#getLayer() + */ + public int getLayer() { + return IAnnotationPresentation.DEFAULT_LAYER; + } + + private Image getImage() { + return isSelected() ? BootDashActivator.getDefault().getImageRegistry().get(BootDashActivator.CHECK_ICON) + : BootDashActivator.getDefault().getImageRegistry().get(BootDashActivator.CHECK_GREYSCALE_ICON); + } + + /** + * Returns the state of this annotation. + * + * @return true if collapsed + */ + public boolean isSelected() { + return fIsSelected; + } + + /** + * Marks this annotation as being selected. + */ + public void markSelected() { + fIsSelected= true; + } + + /** + * Marks this annotation as being unselected. + */ + public void markUnselected() { + fIsSelected= false; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameAnnotationModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameAnnotationModel.java new file mode 100644 index 000000000..5b04ff392 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameAnnotationModel.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2016, 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.cf.deployment; + +import java.util.Iterator; + +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.AnnotationModel; + +/** + * Application Name annotations model + * + * @author Alex Boyko + * + */ +public class AppNameAnnotationModel extends AnnotationModel { + + /** + * Annotation model key for attachment to viewer visual annotations model + */ + public static final Object APP_NAME_MODEL_KEY = new Object(); + + /** + * Constant application name. If not null then corresponding annotation must be selected + */ + public final String fixedAppName; + + /** + * Creates a new, empty projection annotation model. + */ + public AppNameAnnotationModel(String fixedAppName) { + this.fixedAppName = fixedAppName; + } + + /** + * Marks the given annotation as selected. An appropriate + * annotation model change event is sent out. + * + * @param annotation the annotation + */ + public void markSelected(Annotation annotation) { + if (annotation instanceof AppNameAnnotation) { + AppNameAnnotation appName = (AppNameAnnotation) annotation; + Iterator iterator= getAnnotationIterator(); + while(iterator.hasNext()) { + Object o = iterator.next(); + if (o instanceof AppNameAnnotation && appName != o) { + AppNameAnnotation a = (AppNameAnnotation) o; + if (a.isSelected()) { + a.markUnselected(); + modifyAnnotation(a, true); + } + } + } + if (!appName.isSelected()) { + appName.markSelected(); + modifyAnnotation(appName, true); + } + } + } + + /** + * Finds the first selected annotation + * + * @return the selected annotation + */ + public AppNameAnnotation getSelectedAppAnnotation() { + Iterator iterator= getAnnotationIterator(); + while(iterator.hasNext()) { + Object o = iterator.next(); + if (o instanceof AppNameAnnotation) { + AppNameAnnotation a = (AppNameAnnotation) o; + if (a.isSelected()) { + return a; + } + } + } + return null; + } + + /** + * Finds application name annotation corresponding to the passed in + * application name text parameter + * + * @param text Application name text + * @return annotation corresponding to the application name text + */ + public AppNameAnnotation getAnnotation(String text) { + if (text != null) { + Iterator iterator= getAnnotationIterator(); + while(iterator.hasNext()) { + Object o = iterator.next(); + if (o instanceof AppNameAnnotation) { + AppNameAnnotation a = (AppNameAnnotation) o; + if (text.equals(a.getText())) { + return a; + } + } + } + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameAnnotationSupport.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameAnnotationSupport.java new file mode 100644 index 000000000..48ffc32a3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameAnnotationSupport.java @@ -0,0 +1,260 @@ +/******************************************************************************* + * Copyright (c) 2016, 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.cf.deployment; + +import java.util.Iterator; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextInputListener; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.AnnotationPainter; +import org.eclipse.jface.text.source.IAnnotationAccess; +import org.eclipse.jface.text.source.IAnnotationHover; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.IAnnotationModelExtension; +import org.eclipse.jface.text.source.ISharedTextColors; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.jface.text.source.ISourceViewerExtension2; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.custom.StyledTextContent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.RGB; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * Application name annotation support for the {@link ISourceViewer} + * + * @author Alex Boyko + * + */ +public class AppNameAnnotationSupport { + + /** + * Unselected application name annotation line color + */ + private static final RGB APP_NAME_COLOR = new RGB(0xBB, 0xBB, 0xBB); + + /** + * Selected application name annotation line color + */ + private static final RGB SELECTED_APP_NAME_COLOR = new RGB(0x00, 0x87, 0x00); + + /** + * Alpha value for painting application name annotations line highlighting + * (Must be close to 0 to ensure the text behind is visible enough) + */ + private static final int APP_NAME_ANNOTATION_ALPHA = 0x17; + + /** + * Width of the application name annotations vertical ruler control + */ + private static final int ANNOTATION_COLUMN_WIDTH = 12; + + /** + * The source viewer + */ + private SourceViewer fViewer = null; + + /** + * Application name annotations vertical ruler + */ + private AppNameRulerColumn fColumn = null; + + /** + * Constant colors cache (passed in) + */ + private ISharedTextColors fColorsCache; + + private final String fixedAppName; + + /** + * Listen to viewer input changes to attach the application name annotations model if necessary + */ + private ITextInputListener textInputListener = new ITextInputListener() { + + public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) { + } + + @Override + public void inputDocumentChanged(IDocument oldInput, IDocument newInput) { + initAppAnnotationModel(); + } + + }; + + private void initAppAnnotationModel() { + IAnnotationModel annotationModel = fViewer.getVisualAnnotationModel(); + if (annotationModel instanceof IAnnotationModelExtension) { + IAnnotationModelExtension extension = (IAnnotationModelExtension) annotationModel; + if (extension.getAnnotationModel(AppNameAnnotationModel.APP_NAME_MODEL_KEY) == null) { + extension.addAnnotationModel(AppNameAnnotationModel.APP_NAME_MODEL_KEY, new AppNameAnnotationModel(fixedAppName)); + } + } + fColumn.setModel(annotationModel); + } + + public AppNameAnnotationSupport(SourceViewer viewer, IAnnotationAccess annotationAccess, ISharedTextColors colorsCache, String fixedAppName) { + super(); + this.fixedAppName = fixedAppName; + fViewer = viewer; + fViewer.addTextInputListener(textInputListener); + + fColorsCache = colorsCache; + fColumn = new AppNameRulerColumn(ANNOTATION_COLUMN_WIDTH, annotationAccess); + fColumn.addAnnotationType(AppNameAnnotation.TYPE); + + /* + * Setup tooltip for application name annotations + */ + fColumn.setHover(new IAnnotationHover() { + @Override + public String getHoverInfo(ISourceViewer sourceViewer, int lineNumber) { + for (Iterator itr = fColumn.getModel().getAnnotationIterator(); itr.hasNext();) { + Object o = itr.next(); + if (o instanceof AppNameAnnotation) { + AppNameAnnotation a = (AppNameAnnotation) o; + Position p = fColumn.getModel().getPosition(a); + try { + if (fViewer.getDocument().getLineOfOffset(p.getOffset()) <= lineNumber && fViewer.getDocument().getLineOfOffset(p.getOffset() + p.getLength() - 1) >= lineNumber) { + String hoverText = "Application '" + a.getText() + "'"; + if (a.isSelected()) { + hoverText += " is selected"; + } + return hoverText; + } + } catch (BadLocationException e) { + Log.log(e); + } + } + } + return null; + } + + }); + + /* + * Setup application name line highlight painting on the viewer's text widget + */ + AppNameAnnotationsPainter annotationPainter= new AppNameAnnotationsPainter(fViewer, annotationAccess); + annotationPainter.addDrawingStrategy(AppNameAnnotationModel.APP_NAME_MODEL_KEY, new AppNameDrawingStrategy()); + annotationPainter.addAnnotationType(AppNameAnnotation.TYPE, AppNameAnnotationModel.APP_NAME_MODEL_KEY); + annotationPainter.setAnnotationTypeColor(AppNameAnnotation.TYPE, fViewer.getControl().getDisplay().getSystemColor(SWT.COLOR_GRAY)); + initAppAnnotationModel(); + fViewer.addPainter(annotationPainter); + + /* + * Attach application annotations ruler to the viewer + */ + if (fixedAppName == null) { + fViewer.addVerticalRulerColumn(fColumn); + } + } + + public void dispose() { + fViewer.removeTextInputListener(textInputListener); + } + + /** + * Finds and returns viewer's application name annotations model + * + * @param viewer Source viewer + * @return Viewer's application name annotations model + */ + public static AppNameAnnotationModel getAppNameAnnotationModel(ISourceViewer viewer) { + IAnnotationModel model = viewer instanceof ISourceViewerExtension2 + ? ((ISourceViewerExtension2) viewer).getVisualAnnotationModel() : viewer.getAnnotationModel(); + AppNameAnnotationModel appNameModel = null; + if (model instanceof IAnnotationModelExtension) { + appNameModel = (AppNameAnnotationModel) ((IAnnotationModelExtension) model) + .getAnnotationModel(AppNameAnnotationModel.APP_NAME_MODEL_KEY); + } else if (model instanceof AppNameAnnotationModel) { + appNameModel = (AppNameAnnotationModel) model; + } + return appNameModel; + } + + private static class AppNameAnnotationsPainter extends AnnotationPainter { + + /** + * Creates a new painter indicating the location of collapsed regions. + * + * @param sourceViewer the source viewer for the painter + * @param access the annotation access + */ + public AppNameAnnotationsPainter(ISourceViewer sourceViewer, IAnnotationAccess access) { + super(sourceViewer, access); + } + + /* + * @see org.eclipse.jface.text.source.AnnotationPainter#findAnnotationModel(org.eclipse.jface.text.source.ISourceViewer) + */ + protected IAnnotationModel findAnnotationModel(ISourceViewer sourceViewer) { + return getAppNameAnnotationModel(sourceViewer); + } + + /* + * @see org.eclipse.jface.text.source.AnnotationPainter#skip(org.eclipse.jface.text.source.Annotation) + */ + protected boolean skip(Annotation annotation) { + return !(annotation instanceof AppNameAnnotation); + } + } + + private class AppNameDrawingStrategy implements AnnotationPainter.IDrawingStrategy { + /* + * @see org.eclipse.jface.text.source.AnnotationPainter.IDrawingStrategy#draw(org.eclipse.swt.graphics.GC, org.eclipse.swt.custom.StyledText, int, int, org.eclipse.swt.graphics.Color) + */ + public void draw(Annotation annotation, GC gc, StyledText textWidget, int offset, int length, Color color) { + if (annotation instanceof AppNameAnnotation) { + AppNameAnnotation a = (AppNameAnnotation) annotation; + StyledTextContent content = textWidget.getContent(); + final int line = content.getLineAtOffset(offset); + if (gc == null) { + /* + * Clear off highlighting case + */ + textWidget.setLineBackground(line, 1, null); + textWidget.redrawRange(offset, length, true); + } else { + /* + * Show highlighting case IMPORTANT: do not modify + * 'textWidget' graphical parameters! It would start + * scheduling async updates indefinitely! Hence other parts + * of the UI won't have a chance to repaint themselves + */ + Position p = fColumn.getModel().getPosition(annotation); + if (p != null && line == content.getLineAtOffset(p.getOffset())) { + /* + * Draw transparent line highlight rectangle. Ensure + * it's transparent such that text behind it is visible + */ + Color lineColor = a.isSelected() ? fColorsCache.getColor(SELECTED_APP_NAME_COLOR) + : fColorsCache.getColor(APP_NAME_COLOR); + Color c = gc.getBackground(); + int opacity = gc.getAlpha(); + gc.setBackground(lineColor); + gc.setAlpha(APP_NAME_ANNOTATION_ALPHA); + gc.fillRectangle(0, textWidget.getLocationAtOffset(offset).y, textWidget.getClientArea().width, + textWidget.getLineHeight()); + gc.setAlpha(opacity); + gc.setBackground(c); + } + } + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameReconciler.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameReconciler.java new file mode 100644 index 000000000..532b045cc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameReconciler.java @@ -0,0 +1,239 @@ +/******************************************************************************* + * Copyright (c) 2016, 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.cf.deployment; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModelExtension; +import org.springframework.ide.eclipse.editor.support.yaml.ast.YamlASTProvider; +import org.springframework.ide.eclipse.editor.support.yaml.ast.YamlFileAST; +import org.springsource.ide.eclipse.commons.livexp.util.Log; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; +import org.yaml.snakeyaml.parser.ParserException; +import org.yaml.snakeyaml.scanner.ScannerException; + +/** + * Reconciler responsible for creating annotation at application names positions + * in the Deployment YAML document + * + * @author Alex Boyko + * + */ +public class AppNameReconciler { + + /** + * YAML parser + */ + private YamlASTProvider fParser; + + public AppNameReconciler(YamlASTProvider parser) { + fParser = parser; + } + + /** + * Re-populates annotation model with app name annotations based on document contents + * + * @param document The YAML document + * @param annotationModel Application Names annotation model + * @param monitor Progress monitor + */ + public void reconcile(IDocument document, AppNameAnnotationModel annotationModel, IProgressMonitor monitor) { + if (annotationModel == null) { + return; + } + + List toRemove= new ArrayList<>(); + + Iterator iter= annotationModel.getAnnotationIterator(); + while (iter.hasNext()) { + Annotation annotation= iter.next(); + if (AppNameAnnotation.TYPE.equals(annotation.getType())) { + toRemove.add(annotation); + } + } + Annotation[] annotationsToRemove= toRemove.toArray(new Annotation[toRemove.size()]); + + /* + * Create brand new annotation to position map based on docs contents + */ + Map annotationsToAdd = createAnnotations(document, annotationModel, monitor); + + /* + * Update annotation model + */ + if (annotationModel instanceof IAnnotationModelExtension) + ((IAnnotationModelExtension)annotationModel).replaceAnnotations(annotationsToRemove, annotationsToAdd); + else { + for (int i= 0; i < annotationsToRemove.length; i++) + annotationModel.removeAnnotation(annotationsToRemove[i]); + for (iter= annotationsToAdd.keySet().iterator(); iter.hasNext();) { + Annotation annotation= iter.next(); + annotationModel.addAnnotation(annotation, annotationsToAdd.get(annotation)); + } + } + } + + /** + * Create new annotation to position mapping based on the document contents + * + * @param annotationModel Application name annotations model + * @return Map of annotations to their corresponding positions + */ + private Map createAnnotations(IDocument document, AppNameAnnotationModel annotationModel, IProgressMonitor monitor) { + Map annotationsMap = new LinkedHashMap<>(); + monitor.beginTask("Calculating application names", 100); + try { + YamlFileAST ast = fParser.getAST(document); + String contents = document.get(); + List rootList = ast.getNodes(); + monitor.worked(70); + if (rootList.size() == 1) { + Node root = rootList.get(0); + SequenceNode applicationsNode = YamlGraphDeploymentProperties.getNode(root, ApplicationManifestHandler.APPLICATIONS_PROP, SequenceNode.class); + if (applicationsNode == null) { + /* + * No 'applications' YAML node consider root elements to the deployment properties of an application + */ + ScalarNode node = YamlGraphDeploymentProperties.getPropertyValue(root, ApplicationManifestHandler.NAME_PROP, ScalarNode.class); + if (node != null) { + /* + * There is 'name' property present, so yes root has application deployment props + */ + annotationsMap.put(new AppNameAnnotation(node.getValue(), true), + new Position(root.getStartMark().getIndex(), + getLastWhiteCharIndex(contents, root.getEndMark().getIndex()) + - root.getStartMark().getIndex())); + } + } else { + /* + * Go through entries in the 'applications' sequence node + */ + for (Node appNode : applicationsNode.getValue()) { + ScalarNode node = YamlGraphDeploymentProperties.getNode(appNode, ApplicationManifestHandler.NAME_PROP, ScalarNode.class); + if (node != null) { + /* + * Add application name annotation entry + */ + annotationsMap.put(new AppNameAnnotation(node.getValue()), new Position(appNode.getStartMark().getIndex(), getLastWhiteCharIndex(contents, appNode.getEndMark().getIndex()) - appNode.getStartMark().getIndex())); + } + } + } + monitor.worked(20); + if (!annotationsMap.isEmpty()) { + if (annotationModel.fixedAppName == null) { + /* + * Select either previously selected app name annotation or the first found + */ + reselectAnnotation(annotationModel, annotationsMap); + } else { + /* + * Select annotation corresponding to application name == to fAppName + */ + selectAnnotationByAppName(annotationsMap, annotationModel.fixedAppName); + } + monitor.worked(10); + } + } + } catch (ParserException | ScannerException e) { + // Ignore these exceptions as they'd appear as syntax errors in the editor + } catch (Throwable t) { + Log.log(t); + } finally { + monitor.done(); + } + return annotationsMap; + } + + /** + * Selects annotation from the map corresponding to currently selected + * annotation. Otherwise just selects the first found annotation + * + * @param annotationModel Application name annotations model + * @param annotationsMap Map of application name annotations to positions + */ + private void reselectAnnotation(AppNameAnnotationModel annotationModel, Map annotationsMap) { + AppNameAnnotation selected = annotationModel.getSelectedAppAnnotation(); + Map.Entry newSelected = null; + if (selected != null) { + Position selectedPosition = annotationModel.getPosition(selected); + for (Map.Entry entry : annotationsMap.entrySet()) { + /* + * Check if application name matches + */ + if (entry.getKey().getText().equals(selected.getText())) { + /* + * If name matches see if previous match is further away + * from previously selected annotation offset than the + * current match. Update the match accordingly. + */ + if (newSelected == null) { + newSelected = entry; + } else if (Math.abs(newSelected.getValue().getOffset() - selectedPosition.getOffset()) > Math.abs(entry.getValue().getOffset() - selectedPosition.getOffset())){ + newSelected = entry; + } + } else if (entry.getValue().getOffset() == selectedPosition.getOffset() && newSelected == null) { + newSelected = entry; + } + } + } + if (newSelected == null) { + /* + * No matches found. Select the first annotation to have something selected. + */ + newSelected = annotationsMap.entrySet().iterator().next(); + } + newSelected.getKey().markSelected(); + } + + /** + * Select annotation matching constant application name, i.e. FAppName + * + * @param annotationsMap Map of application name annotations to positions + */ + private void selectAnnotationByAppName(Map annotationsMap, String appName) { + for (Map.Entry entry : annotationsMap.entrySet()) { + if (entry.getKey().getText().equals(appName)) { + entry.getKey().markSelected(); + return; + } + } + } + + /** + * Returns the first 'white' char after a word appearing before the passed index + * + * @param text Text + * @param index Index to start looking from + * @return The first 'white' char position in a string + */ + private static int getLastWhiteCharIndex(String text, int index) { + if (index == text.length()) { + return index; + } + int i = index; + for (; i >= 0 && Character.isWhitespace(text.charAt(i)); i--) { + // Nothing to do + } + // Special case: if non white char is at position 'index' then return value of 'index' + return i == index ? i : i + 1; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameReconcilingStrategy.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameReconcilingStrategy.java new file mode 100644 index 000000000..33f5aba68 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameReconcilingStrategy.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (c) 2016, 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.cf.deployment; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.reconciler.DirtyRegion; +import org.eclipse.jface.text.reconciler.IReconcilingStrategy; +import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.IAnnotationModelExtension; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.jface.text.source.ISourceViewerExtension2; +import org.springframework.ide.eclipse.editor.support.yaml.ast.YamlASTProvider; + +/** + * Reconciling strategy responsible for keeping track of application name + * annotations + * + * @author Alex Boyko + * + */ +public class AppNameReconcilingStrategy implements IReconcilingStrategy, IReconcilingStrategyExtension { + + /** + * Reconciler for application name annotations + */ + private AppNameReconciler fReconciler; + + /** + * Source viewer + */ + private ISourceViewer fViewer; + + /** + * Document to perform reconciling on + */ + private IDocument fDocument; + + /** + * Reconciling cycle progress monitor + */ + private IProgressMonitor fProgressMonitor; + + /** + * Creates new instance of the reconciler + * + * @param viewer Source viewer + * @param parser YAML parser + * @param appName Application name to keep selected all the time + */ + public AppNameReconcilingStrategy(YamlASTProvider parser) { + fReconciler = new AppNameReconciler(parser); + } + + /* + * @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#initialReconcile() + */ + public void initialReconcile() { + reconcile(new Region(0, fDocument.getLength())); + } + + /* + * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#reconcile(org.eclipse.jface.text.reconciler.DirtyRegion,org.eclipse.jface.text.IRegion) + */ + public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) { + try { + IRegion startLineInfo= fDocument.getLineInformationOfOffset(subRegion.getOffset()); + IRegion endLineInfo= fDocument.getLineInformationOfOffset(subRegion.getOffset() + Math.max(0, subRegion.getLength() - 1)); + if (startLineInfo.getOffset() == endLineInfo.getOffset()) + subRegion= startLineInfo; + else + subRegion= new Region(startLineInfo.getOffset(), endLineInfo.getOffset() + Math.max(0, endLineInfo.getLength() - 1) - startLineInfo.getOffset()); + + } catch (BadLocationException e) { + subRegion= new Region(0, fDocument.getLength()); + } + reconcile(subRegion); + } + + private AppNameAnnotationModel getAppNameAnnotationModel() { + IAnnotationModel model = fViewer instanceof ISourceViewerExtension2 ? ((ISourceViewerExtension2)fViewer).getVisualAnnotationModel() : fViewer.getAnnotationModel(); + if (model instanceof IAnnotationModelExtension) { + return (AppNameAnnotationModel) ((IAnnotationModelExtension) model).getAnnotationModel(AppNameAnnotationModel.APP_NAME_MODEL_KEY); + } + return (AppNameAnnotationModel) model; + } + + @Override + public void reconcile(IRegion region) { + fReconciler.reconcile(fDocument, getAppNameAnnotationModel(), fProgressMonitor); + } + + /* + * @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#setProgressMonitor(org.eclipse.core.runtime.IProgressMonitor) + */ + public final void setProgressMonitor(IProgressMonitor monitor) { + fProgressMonitor= monitor; + } + + @Override + public void setDocument(IDocument document) { + fDocument= document; + } + + public void install(ISourceViewer viewer) { + fViewer = viewer; + } + + public void uninstall() { + fViewer = null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameRulerColumn.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameRulerColumn.java new file mode 100644 index 000000000..a0b14cf5d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/AppNameRulerColumn.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.deployment; + +import java.util.Iterator; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.source.AnnotationRulerColumn; +import org.eclipse.jface.text.source.CompositeRuler; +import org.eclipse.jface.text.source.IAnnotationAccess; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.IAnnotationModelExtension; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.MouseTrackAdapter; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** + * Application name annotations ruler control. Implementation based on + * ProjectionRulerColumn class implementation + * + * @author Alex Boyko + * + */ +public class AppNameRulerColumn extends AnnotationRulerColumn { + + /** + * Currently examined/hovered over annotation + */ + private AppNameAnnotation fCurrentAnnotation; + + /** + * Line number recorded on mouse down. + */ + private int fMouseDownLine; + + public AppNameRulerColumn(IAnnotationModel model, int width, IAnnotationAccess annotationAccess) { + super(model, width, annotationAccess); + } + + public AppNameRulerColumn(int width, IAnnotationAccess annotationAccess) { + super(width, annotationAccess); + } + + @Override + protected void mouseClicked(int line) { + clearCurrentAnnotation(); + if (fMouseDownLine != line) + return; + AppNameAnnotation annotation= findAnnotation(line, true); + if (annotation != null) { + AppNameAnnotationModel model= (AppNameAnnotationModel) getModel(); + model.markSelected(annotation); + } + } + + @Override + protected void mouseDown(int rulerLine) { + fMouseDownLine= rulerLine; + } + + @Override + protected void mouseDoubleClicked(int rulerLine) { + if (findAnnotation(rulerLine, true) != null) + return; + + AppNameAnnotation annotation= findAnnotation(rulerLine, false); + if (annotation != null) { + AppNameAnnotationModel model= (AppNameAnnotationModel) getModel(); + model.markSelected(annotation); + } + } + + /** + * Returns the app's name annotation of the column's annotation + * model that contains the given line. + * + * @param line the line + * @param exact true if the annotation range must match exactly + * @return the app name annotation containing the given line + */ + private AppNameAnnotation findAnnotation(int line, boolean exact) { + + AppNameAnnotation previousAnnotation= null; + + IAnnotationModel model= getModel(); + if (model != null) { + IDocument document= getCachedTextViewer().getDocument(); + + int previousDistance= Integer.MAX_VALUE; + + Iterator e= model.getAnnotationIterator(); + while (e.hasNext()) { + Object next= e.next(); + if (next instanceof AppNameAnnotation) { + AppNameAnnotation annotation= (AppNameAnnotation) next; + Position p= model.getPosition(annotation); + if (p == null) + continue; + + int distance= getDistance(annotation, p, document, line); + if (distance == -1) + continue; + + if (!exact) { + if (distance < previousDistance) { + previousAnnotation= annotation; + previousDistance= distance; + } + } else if (distance == 0) { + previousAnnotation= annotation; + } + } + } + } + + return previousAnnotation; + } + + /** + * Returns the distance of the given line to the start line of the given position in the given document. The distance is + * -1 when the line is not included in the given position. + * + * @param annotation the annotation + * @param position the position + * @param document the document + * @param line the line + * @return -1 if line is not contained, a position number otherwise + */ + private int getDistance(AppNameAnnotation annotation, Position position, IDocument document, int line) { + if (position.getOffset() > -1 && position.getLength() > -1) { + try { + int startLine= document.getLineOfOffset(position.getOffset()); + int endLine= document.getLineOfOffset(position.getOffset() + position.getLength()); + if (startLine <= line && line < endLine) { + return line - startLine; + } + } catch (BadLocationException x) { + } + } + return -1; + } + + private boolean clearCurrentAnnotation() { + if (fCurrentAnnotation != null) { + fCurrentAnnotation.setRangeIndication(false); + fCurrentAnnotation= null; + return true; + } + return false; + } + + @Override + public Control createControl(CompositeRuler parentRuler, Composite parentControl) { + Control control= super.createControl(parentRuler, parentControl); + + // set background + Color background= getCachedTextViewer().getTextWidget().getBackground(); + control.setBackground(background); + + // install hover listener + control.addMouseTrackListener(new MouseTrackAdapter() { + public void mouseExit(MouseEvent e) { + if (clearCurrentAnnotation()) + redraw(); + } + }); + + // install mouse move listener + control.addMouseMoveListener(new MouseMoveListener() { + public void mouseMove(MouseEvent e) { + boolean redraw= false; + AppNameAnnotation annotation= findAnnotation(toDocumentLineNumber(e.y), false); + if (annotation != fCurrentAnnotation) { + if (fCurrentAnnotation != null) { + fCurrentAnnotation.setRangeIndication(false); + redraw= true; + } + fCurrentAnnotation= annotation; + if (fCurrentAnnotation != null) { + fCurrentAnnotation.setRangeIndication(true); + redraw= true; + } + } + if (redraw) + redraw(); + } + }); + return control; + } + + @Override + public void setModel(IAnnotationModel model) { + if (!(model instanceof AppNameAnnotationModel) && model instanceof IAnnotationModelExtension) { + IAnnotationModelExtension extension= (IAnnotationModelExtension) model; + model= extension.getAnnotationModel(AppNameAnnotationModel.APP_NAME_MODEL_KEY); + } + super.setModel(model); + } + + /* + * @see org.eclipse.jface.text.source.AnnotationRulerColumn#isPropagatingMouseListener() + */ + protected boolean isPropagatingMouseListener() { + return false; + } + + /* + * @see org.eclipse.jface.text.source.AnnotationRulerColumn#hasAnnotation(int) + */ + protected boolean hasAnnotation(int lineNumber) { + return findAnnotation(lineNumber, true) != null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/ApplicationManifestHandler.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/ApplicationManifestHandler.java new file mode 100644 index 000000000..9ce8f8b01 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/ApplicationManifestHandler.java @@ -0,0 +1,1019 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.deployment; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.SubMonitor; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFDomainType; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFStack; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFDomainStatus; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFRoute; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeploymentProperties; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.DumperOptions.FlowStyle; +import org.yaml.snakeyaml.Yaml; + +import com.google.common.collect.ImmutableList; + +/** + * Reads and creates manifest.yml content from a specific location relative to + * an {@link IProject}. + * + */ +public class ApplicationManifestHandler { + + public static final String RANDOM_VAR = "${random}"; //$NON-NLS-1$ + + public static final String APPLICATIONS_PROP = "applications"; + + public static final String NAME_PROP = "name"; + + public static final String MEMORY_PROP = "memory"; + + public static final String INSTANCES_PROP = "instances"; + + public static final String SUB_DOMAIN_PROP = "host"; + + public static final String SUB_DOMAINS_PROP = "hosts"; + + public static final String DOMAIN_PROP = "domain"; + + public static final String DOMAINS_PROP = "domains"; + + public static final String NO_ROUTE_PROP = "no-route"; + + public static final String NO_HOSTNAME_PROP = "no-hostname"; + + public static final String RANDOM_ROUTE_PROP = "random-route"; + + public static final String SERVICES_PROP = "services"; + + public static final String LABEL_PROP = "label"; + + public static final String PROVIDER_PROP = "provider"; + + public static final String VERSION_PROP = "version"; + + public static final String PLAN_PROP = "plan"; + + public static final String PATH_PROP = "path"; + + public static final String BUILDPACK_PROP = "buildpack"; + + public static final String BUILDPACKS_PROP = "buildpacks"; + + public static final String ENV_PROP = "env"; + + public static final String DISK_QUOTA_PROP = "disk_quota"; + + public static final String INHERIT_PROP = "inherit"; + + public static final String TIMEOUT_PROP = "timeout"; + + public static final String HEALTH_CHECK_TYPE_PROP = "health-check-type"; + + public static final String HEALTH_CHECK_HTTP_ENDPOINT_PROP = "health-check-http-endpoint"; + + public static final String COMMAND_PROP = "command"; + + public static final String STACK_PROP = "stack"; + + public static final String ROUTES_PROP = "routes"; + + public static final String ROUTE_PROP = "route"; + + private final IProject project; + + private final IFile manifestFile; + + private final CloudData cloudData; + + public ApplicationManifestHandler(IProject project, CloudData cloudData) { + this(project, cloudData, null); + } + + public ApplicationManifestHandler(IProject project, CloudData cloudData, IFile manifestFile) { + this.project = project; + this.manifestFile = manifestFile; + this.cloudData = cloudData; + } + + protected InputStream getInputStream() throws Exception { + + File file = getManifestFile(); + if (file != null && file.exists()) { + return new FileInputStream(file); + } else { + throw ExceptionUtil.coreException("No manifest.yml file found in project: " + project.getName()); + } + } + + /** + * + * @return manifest file if it exists. Null otherwise + */ + public File getManifestFile() { + if (manifestFile != null) { + URI locationURI = manifestFile.getLocationURI(); + if (locationURI != null) { + File file = new File(locationURI); + return file.exists() ? file : null; + } + } + return null; + } + + public boolean hasManifest() { + File file = getManifestFile(); + return file != null && file.exists(); + } + + /** + * + * @param applicationName + * name of application to lookup in the manifest file. + * @param propertyName + * String value property to retrieve from manifest for given + * application entry. + * @return Value of property, or null if not found, or entry for application + * in manifest does not exist. + */ + public String getApplicationProperty(String appName, String propertyName, IProgressMonitor monitor) { + try { + Map appMap = getApplicationMap(appName, monitor); + if (appMap != null) { + return getValue(appMap, propertyName, String.class); + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + public Map getApplicationMap(String appName, IProgressMonitor monitor) throws Exception { + Map allResults = parseManifestFromFile(); + + List> appMaps = getApplications(allResults); + + for (Map appMap : appMaps) { + + String existingAppName = getValue(appMap, NAME_PROP, String.class); + if (existingAppName != null && existingAppName.equals(appName)) { + return appMap; + } + } + return null; + } + + /** + * + * @param containerMap + * @param propertyName + * @return map of values for the given property name, or null if it cannot + * be resolved + */ + @SuppressWarnings("unchecked") + protected Map getContainingPropertiesMap(Map containerMap, String propertyName) { + if (containerMap == null || propertyName == null) { + return null; + } + Object yamlElementObj = containerMap.get(propertyName); + + if (yamlElementObj instanceof Map) { + return (Map) yamlElementObj; + } else { + return null; + } + } + +// protected String getStringValue(Map containingMap, String propertyName) { +// +// if (containingMap == null) { +// return null; +// } +// +// Object valObj = containingMap.get(propertyName); +// +// if (valObj instanceof String) { +// return (String) valObj; +// } +// return null; +// } +// + @SuppressWarnings("unchecked") + protected static T getValue(Map containingMap, String propertyName, Class type) { + if (containingMap == null) { + return null; + } + Object valObj = containingMap.get(propertyName); + + if (valObj != null && type.isAssignableFrom(valObj.getClass())) { + return (T) valObj; + } + return null; + } + + protected static boolean hasValue(Map containingMap, String propertyName) { + return containingMap != null && containingMap.containsKey(propertyName); + } + + public static List> getApplications(Map results) throws CoreException { + + Object applicationsObj = results.get(APPLICATIONS_PROP); + if (!(applicationsObj instanceof List)) { + return null; + } + + List applicationsList = (List) applicationsObj; + + List> applications = new ArrayList<>(); + + // Use only the first application entry + if (!applicationsList.isEmpty()) { + for (Object val : applicationsList) { + if (val instanceof Map) { + applications.add((Map) val); + } + + } + } + + return applications; + } + + protected CloudApplicationDeploymentProperties getDeploymentProperties(Map appMap, + Map allResults, IProgressMonitor monitor) throws Exception { + + CloudApplicationDeploymentProperties properties = new CloudApplicationDeploymentProperties(); + + String appName = getValue(appMap, NAME_PROP, String.class); + + properties.setAppName(appName); + properties.setProject(project); + properties.setManifestFile(manifestFile); + + readMemory(appMap, allResults, properties); + + readDiskQuota(appMap, allResults, properties); + + readApplicationURL(appMap, allResults, properties); + + readBuildpack(appMap, allResults, properties); + + readBuildpacks(appMap, allResults, properties); + + readEnvVars(appMap, allResults, properties); + + readServices(appMap, allResults, properties); + + readInstances(appMap, allResults, properties); + + readTimeout(appMap, allResults, properties); + + readHealthCheckType(appMap, allResults, properties); + + readHealthCheckHttpEndpoint(appMap, allResults, properties); + + readCommand(appMap, allResults, properties); + + readStack(appMap, allResults, properties); + + return properties; + } + + public List load(IProgressMonitor monitor) throws Exception { + SubMonitor subMonitor = SubMonitor.convert(monitor); + subMonitor.beginTask("Loading manifest.yml", 6); + + try { + + Map allResults = parseManifestFromFile(); + + if (allResults == null || allResults.isEmpty()) { + throw ExceptionUtil + .coreException("No content found in manifest.yml. Make sure the manifest is valid."); + + } + + List> appMaps = getApplications(allResults); + + List properties = new ArrayList<>(); + + if (appMaps == null) { + CloudApplicationDeploymentProperties props = getDeploymentProperties(allResults, allResults, subMonitor); + if (props != null) { + properties.add(props); + } + } else { + for (Map app : appMaps) { + CloudApplicationDeploymentProperties props = getDeploymentProperties(app, allResults, subMonitor); + if (props != null) { + properties.add(props); + } + } + } + + return properties; + + } finally { + subMonitor.done(); + } + + } + + /** + * Creates a new manifest.yml file. If one already exists, the existing one + * will not be replaced. + * + * @return true if new file created with content. False otherwise + * @throws Exception + * if error occurred during file creation or serialising + * manifest content + */ + public boolean create(IProgressMonitor monitor, CloudApplicationDeploymentProperties properties) throws Exception { + + if (properties == null) { + return false; + } + File file = getManifestFile(); + if (file != null) { + Log.warn( + "Manifest.yml file already found at: " + manifestFile.getFullPath() + ". New content will not be written."); + return false; + } + + Map deploymentInfoYaml = toYaml(properties, cloudData); + DumperOptions options = new DumperOptions(); + options.setExplicitStart(true); + options.setCanonical(false); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(FlowStyle.BLOCK); + Yaml yaml = new Yaml(options); + String manifestValue = yaml.dump(deploymentInfoYaml); + + if (manifestValue == null) { + throw ExceptionUtil.coreException("Failed to generate manifesty.yml for: " + properties.getAppName() + + " Unknown problem trying to serialise content of manifest into: " + deploymentInfoYaml); + } + + createFile(project, manifestFile, manifestValue, monitor); + return true; + } + + public static Map toYaml(DeploymentProperties properties, CloudData cloudData) { + return toYaml(properties, cloudData, false); + } + + @SuppressWarnings("unchecked") + public static Map toYaml(DeploymentProperties properties, CloudData cloudData, boolean legacyHostDomain) { + Map deploymentInfoYaml = new LinkedHashMap<>(); + + Object applicationsObj = deploymentInfoYaml.get(APPLICATIONS_PROP); + List> applicationsList = null; + if (applicationsObj == null) { + applicationsList = new ArrayList<>(); + deploymentInfoYaml.put(APPLICATIONS_PROP, applicationsList); + } else if (applicationsObj instanceof List) { + applicationsList = (List>) applicationsObj; + } + + Map application = new LinkedHashMap<>(); + applicationsList.add(application); + + application.put(NAME_PROP, properties.getAppName()); + + String memory = getMemoryAsString(properties.getMemory()); + if (memory != null) { + application.put(MEMORY_PROP, memory); + } + + String diskQuota = getMemoryAsString(properties.getDiskQuota()); + if (diskQuota != null && properties.getDiskQuota() != DeploymentProperties.DEFAULT_MEMORY) { + application.put(DISK_QUOTA_PROP, diskQuota); + } + + if (properties.getInstances() != DeploymentProperties.DEFAULT_INSTANCES) { + application.put(ApplicationManifestHandler.INSTANCES_PROP, properties.getInstances()); + } + if (properties.getTimeout() != null) { + application.put(ApplicationManifestHandler.TIMEOUT_PROP, properties.getTimeout()); + } + String healthCheck = properties.getHealthCheckType(); + if (healthCheck != null) { + application.put(ApplicationManifestHandler.HEALTH_CHECK_TYPE_PROP, healthCheck); + } + String healthCheckHttpEndpoint = properties.getHealthCheckHttpEndpoint(); + if (healthCheckHttpEndpoint != null) { + application.put(ApplicationManifestHandler.HEALTH_CHECK_HTTP_ENDPOINT_PROP, healthCheckHttpEndpoint); + } + if (properties.getCommand() != null) { + application.put(ApplicationManifestHandler.COMMAND_PROP, properties.getCommand()); + } + if (properties.getStack() != null) { + application.put(ApplicationManifestHandler.STACK_PROP, properties.getStack()); + } + if (properties.getServices() != null && !properties.getServices().isEmpty()) { + application.put(SERVICES_PROP, properties.getServices()); + } + if (properties.getEnvironmentVariables() != null && !properties.getEnvironmentVariables().isEmpty()) { + application.put(ENV_PROP, properties.getEnvironmentVariables()); + } + if (properties.getBuildpack() != null) { + application.put(ApplicationManifestHandler.BUILDPACK_PROP, properties.getBuildpack()); + } + + if (legacyHostDomain) { + Set hosts = new LinkedHashSet<>(); + Set domains = new LinkedHashSet<>(); + List cloudDomains = cloudData.getDomains(); + extractHostsAndDomains(properties.getUris(), cloudDomains, hosts, domains); + for (String uri : properties.getUris()) { + try { + // Find the first valid URL + CFRoute route = CFRoute.builder().from(uri, cloudDomains).build(); + if (route.getHost() != null) { + hosts.add(route.getHost()); + } + if (route.getDomain() != null) { + domains.add(route.getDomain()); + } + } catch (Exception e) { + // ignore + } + } + if (hosts.isEmpty() && domains.isEmpty()) { + application.put(NO_ROUTE_PROP, true); + } else { + if (hosts.isEmpty()) { + application.put(NO_HOSTNAME_PROP, true); + } else if (hosts.size() == 1) { + String host = hosts.iterator().next(); + if (!properties.getAppName().equals(host)) { + application.put(SUB_DOMAIN_PROP, host); + } + } else { + application.put(SUB_DOMAINS_PROP, new ArrayList<>(hosts)); + } + if (domains.size() == 1) { + application.put(DOMAIN_PROP, domains.iterator().next()); + } else if (domains.size() > 1) { + application.put(DOMAINS_PROP, new ArrayList<>(domains)); + } + } + } else { + Set uris = properties.getUris(); + if (uris == null || uris.isEmpty()) { + application.put(NO_ROUTE_PROP, true); + } else { + application.put(ApplicationManifestHandler.ROUTES_PROP, uris.stream().map(uri -> { + Map route = new LinkedHashMap<>(); + route.put(ApplicationManifestHandler.ROUTE_PROP, uri); + return route; + }).collect(Collectors.toList())); + } + } + + return deploymentInfoYaml; + } + + public static void extractHostsAndDomains(Collection uris, List cloudDomains, Set hostsSet, Set domainsSet) { + for (String uri : uris) { + try { + // Find the first valid URL + CFRoute route = CFRoute.builder().from(uri, cloudDomains).build(); + if (route.getHost() != null) { + hostsSet.add(route.getHost()); + } + if (route.getDomain() != null) { + domainsSet.add(route.getDomain()); + } + } catch (Exception e) { + // ignore + } + } + } + + public void createFile(IProject project, IFile file, String data, IProgressMonitor monitor) throws CoreException { + file.create(new ByteArrayInputStream(data.getBytes()), true, monitor); + project.refreshLocal(IResource.DEPTH_INFINITE, monitor); + } + + protected void readEnvVars(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + Map propertiesMap = new LinkedHashMap<>(); + Map map = getContainingPropertiesMap(allResults, ENV_PROP); + if (map != null) { + propertiesMap.putAll(map); + } + map = getContainingPropertiesMap(application, ENV_PROP); + if (map != null) { + propertiesMap.putAll(map); + } + + if (propertiesMap.isEmpty()) { + return; + } + + Map loadedVars = new HashMap<>(); + + for (Entry entry : propertiesMap.entrySet()) { + if ((entry.getKey() instanceof String)) { + String varName = (String) entry.getKey(); + String varValue = null; + if (entry.getValue() instanceof String) { + varValue = (String) entry.getValue(); + } else if (entry.getValue() instanceof Integer) { + varValue = Integer.toString((Integer) entry.getValue()); + } + if (varName != null && varValue != null) { + loadedVars.put(varName, varValue); + } + } + } + properties.setEnvironmentVariables(loadedVars); + } + + protected void readServices(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + + Object yamlElementObj = allResults.get(SERVICES_PROP); + List cloudServices = new ArrayList<>(); + + if (yamlElementObj instanceof List) { + addTo((List) yamlElementObj, cloudServices); + } + + yamlElementObj = application.get(SERVICES_PROP); + if (yamlElementObj instanceof List) { + addTo((List) yamlElementObj, cloudServices); + } + + properties.setServices(cloudServices); + } + + protected void readBuildpacks(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + + Object yamlElementObj = allResults.get(BUILDPACKS_PROP); + List buildpacks = new ArrayList<>(); + + if (yamlElementObj instanceof List) { + addTo((List) yamlElementObj, buildpacks); + } + + yamlElementObj = application.get(BUILDPACKS_PROP); + if (yamlElementObj instanceof List) { + addTo((List) yamlElementObj, buildpacks); + } + + properties.setBuildpacks(buildpacks); + } + + protected void addTo(List from, List to) { + + for (Object obj : from) { + if (obj instanceof String && !to.contains(obj)) { + String val = (String) obj; + to.add(val); + } + } + } + + protected void readInstances(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + + Integer instances = getValue(application, INSTANCES_PROP, Integer.class); + if (instances == null) { + instances = getValue(allResults, INSTANCES_PROP, Integer.class); + } + if (instances != null) { + properties.setInstances(instances); + } + } + + protected void readTimeout(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + + Integer timeout = getValue(application, TIMEOUT_PROP, Integer.class); + if (timeout == null) { + timeout = getValue(allResults, TIMEOUT_PROP, Integer.class); + } + if (timeout != null) { + properties.setTimeout(timeout); + } + } + + private void readHealthCheckType(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + String hc = getValue(application, HEALTH_CHECK_TYPE_PROP, String.class); + if (hc == null) { + hc = getValue(allResults, HEALTH_CHECK_TYPE_PROP, String.class); + } + if (hc != null) { + properties.setHealthCheckType(hc); + } + } + + private void readHealthCheckHttpEndpoint(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + String hche = getValue(application, HEALTH_CHECK_HTTP_ENDPOINT_PROP, String.class); + if (hche == null) { + hche = getValue(allResults, HEALTH_CHECK_HTTP_ENDPOINT_PROP, String.class); + } + if (hche != null) { + properties.setHealthCheckHttpEndpoint(hche); + } + } + + protected void readBuildpack(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + + String buildpack = getValue(application, BUILDPACK_PROP, String.class); + if (buildpack == null) { + buildpack = getValue(allResults, BUILDPACK_PROP, String.class); + } + if (buildpack != null) { + properties.setBuildpack(buildpack); + } + } + + protected void readCommand(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + + String command = getValue(application, COMMAND_PROP, String.class); + if (command == null) { + command = getValue(allResults, COMMAND_PROP, String.class); + } + if (command != null) { + properties.setCommand(command); + } + } + + protected void readStack(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + + String stack = getValue(application, STACK_PROP, String.class); + if (stack == null) { + stack = getValue(allResults, STACK_PROP, String.class); + } + if (stack != null && isStackValid(stack, cloudData.getStacks())) { + properties.setStack(stack); + } + } + + private boolean noRoute(Map application, Map allResults) { + Boolean noRoute = getValue(application, NO_ROUTE_PROP, Boolean.class); + if (noRoute == null) { + noRoute = getValue(allResults, NO_ROUTE_PROP, Boolean.class); + } + return Boolean.TRUE.equals(noRoute); + } + + /** + * + * @param application + * @param allResults + * @param properties + * @param randomRoute true if random host should be generated + * @return non-null list of URIs parsed from domains and hosts. May be empty + */ + @SuppressWarnings("unchecked") + private List fromDomainsAndHosts(Map application, Map allResults, + CloudApplicationDeploymentProperties properties, boolean randomRoute) { + + HashSet hostsSet = new LinkedHashSet<>(); + HashSet domainsSet = new LinkedHashSet<>(); + + /* + * Gather domains from app node and root node from 'domain' and 'domains' attributes + */ + String domain = getValue(application, DOMAIN_PROP, String.class); + if (domain == null) { + domain = getValue(allResults, DOMAIN_PROP, String.class); + } + if (domain != null) { + domainsSet.add(domain); + } + List domainList = (List) getValue(allResults, DOMAINS_PROP, List.class); + if (domainList != null) { + domainsSet.addAll(domainList); + } + domainList = (List) getValue(application, DOMAINS_PROP, List.class); + if (domainList != null) { + domainsSet.addAll(domainList); + } + + /* + * Gather domains from app node and root node from 'host' and 'hosts' + * attributes. Account for ${random} in host's name + */ + String host = getValue(application, SUB_DOMAIN_PROP, String.class); + if (host == null) { + host = getValue(allResults, SUB_DOMAIN_PROP, String.class); + } + if (host != null) { + hostsSet.add(host); + } + List hostList = (List) getValue(allResults, SUB_DOMAINS_PROP, List.class); + if (hostList != null) { + hostsSet.addAll(hostList); + } + hostList = (List) getValue(application, SUB_DOMAINS_PROP, List.class); + if (hostList != null) { + hostsSet.addAll(hostList); + } + + /* + * If no host names found check for "random-route: true" and + * "no-hostname: true" otherwise take app name as the host name + */ + if (hostsSet.isEmpty()) { + if (randomRoute) { + //hostsSet.add(extractHost("${random}", 10)); + if (domainsSet.isEmpty()) { + domainsSet.add(cloudData.getDefaultDomain()); + } + } else { + Boolean noHostName = getValue(application, NO_HOSTNAME_PROP, Boolean.class); + if (noHostName == null) { + noHostName = getValue(allResults, NO_HOSTNAME_PROP, Boolean.class); + } + if (!Boolean.TRUE.equals(noHostName)) { + /* + * Assumes name is set before URIs are processed + */ + hostsSet.add(properties.getAppName()); + } + } + } + + /* + * Set a domain if they are still empty + */ + if (domainsSet.isEmpty()) { + domainsSet.add(cloudData.getDefaultDomain()); + } + + /* + * Compose URIs for application based on hosts and domains + */ + List uris = new ArrayList<>(hostsSet.isEmpty() ? 1 : hostsSet.size() * domainsSet.size()); + for (String d : domainsSet) { + if (hostsSet.isEmpty()) { + uris.add(CFRoute.builder().domain(d).build().getRoute()); + } else { + for (String h : hostsSet) { + uris.add(CFRoute.builder().host(h).domain(d).build().getRoute()); + } + } + } + + return uris; + } + + protected void readApplicationURL(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + Boolean randomRoute = getValue(application, RANDOM_ROUTE_PROP, Boolean.class); + if (randomRoute == null) { + randomRoute = getValue(allResults, RANDOM_ROUTE_PROP, Boolean.class); + } + boolean useRandomRoute = Boolean.TRUE.equals(randomRoute); + properties.setRandomRoute(useRandomRoute); + + /* + * Check for "no-route: true". If set then uris list should be empty + */ + if (!noRoute(application, allResults)) { + // Manifest documentation states: + // "The routes attribute cannot be used in conjunction with the + // following + // attributes: host, hosts, domain, domains, and no-hostname. + // An error will result." + // If only routes are available, then only parse routes. Do NOT + // also create a default URI from domain and app name if routes are available. + // This appears to be consistent with cf CLI behaviour as well. + // Otherwise fall back to parsing from domains and hosts + List uris = null; + if (hasRoutesProperty(application, allResults)) { + uris = fromRoutesProperty(application, allResults, properties); + } else { + uris = fromDomainsAndHosts(application, allResults, properties, useRandomRoute); + } + properties.setUris(uris); + } + } + + private List fromRoutesProperty(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) { + Set uris = new LinkedHashSet<>(); + List domains = cloudData.getDomains(); + List routes = getValue(application, ROUTES_PROP, List.class); + List rootRoutes = getValue(allResults, ROUTES_PROP, List.class); + if (routes != null || rootRoutes != null) { + routes = routes == null ? Collections.emptyList() : routes; + rootRoutes = rootRoutes == null ? Collections.emptyList() : rootRoutes; + + return ImmutableList.copyOf(Stream.concat(routes.stream(), rootRoutes.stream()) + .filter(o -> o instanceof Map) + .map(o -> (Map) o) + .map(routeMap -> routeMap.get(ROUTE_PROP)) + .filter(Objects::nonNull) + .filter(route -> route instanceof String) + .map(route -> { + String url = (String) route; + CFRoute rt = CFRoute.builder().from(url, domains).build(); + return rt.getRoute(); + }) + .collect(Collectors.toSet())); + } + return ImmutableList.copyOf(uris); + } + + protected boolean hasRoutesProperty(Map application, Map allResults) { + return hasValue(application, ROUTES_PROP) || hasValue(allResults, ROUTES_PROP); + } + + public static boolean isStackValid(String stack, List stacks) { + for (CFStack cloudStack : stacks) { + if (cloudStack.getName().equals(stack)) { + return true; + } + } + return false; + } + + private String extractHost(String subdomain, int length) { + // Check for random word + int varIndex = subdomain.indexOf(RANDOM_VAR); + while (varIndex >= 0) { + String randomWord = RandomStringUtils.randomAlphabetic(length); + subdomain = subdomain.replace(subdomain.substring(varIndex, RANDOM_VAR.length()), randomWord); + varIndex = subdomain.indexOf(RANDOM_VAR); + } + return subdomain; + } + + protected void readMemory(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) throws Exception { + int memoryValue = readMemoryValue(application, allResults, MEMORY_PROP); + if (memoryValue >= 0) { + properties.setMemory(memoryValue); + } + } + + protected void readDiskQuota(Map application, Map allResults, + CloudApplicationDeploymentProperties properties) throws Exception { + int memoryValue = readMemoryValue(application, allResults, DISK_QUOTA_PROP); + if (memoryValue >= 0) { + properties.setDiskQuota(memoryValue); + } + } + + protected int readMemoryValue(Map application, Map allResults, + String propertyKey) throws Exception { + // Check if value in integer form + Integer memoryVal = getValue(application, propertyKey, Integer.class); + if (memoryVal != null) { + return memoryVal.intValue(); + } + + // If not in Integer form, try String as the memory may end in with a 'G' or 'M' + String memoryStringVal = getValue(application, propertyKey, String.class); + if (memoryStringVal == null) { + // Check if there is memory property set for all apps if nothing set for the app specifically + memoryVal = getValue(allResults, propertyKey, Integer.class); + if (memoryVal == null) { + // Not in integer form + memoryStringVal = getValue(allResults, propertyKey, String.class); + } else { + // Integer form? Return the value right away + return memoryVal.intValue(); + } + } + + // Should only get here if memory value not in integer form + if (memoryStringVal != null && memoryStringVal.length() > 0) { + // Parse non-integer memory value + return convertMemory(memoryStringVal); + } + + // No memory property specified? Assume the default + return DeploymentProperties.DEFAULT_MEMORY; + } + + public static int convertMemory(String memoryStringVal) throws CoreException { + String memoryIndicator[] = { "m", "g", "mb", "gb" }; + int gIndex = -1; + boolean gb = false; + + for (String indicator : memoryIndicator) { + int beginIndex = memoryStringVal.length() - indicator.length(); + if (beginIndex >= 0) { + if (indicator.equalsIgnoreCase(memoryStringVal.substring(beginIndex))) { + gIndex = beginIndex; + gb = indicator.charAt(0) == 'g'; + break; + } + } + } + + // There has to be a number before the 'G' or 'M', if 'G' or 'M' + // is used, or its not a valid + // memory + if (gIndex > 0) { + memoryStringVal = memoryStringVal.substring(0, gIndex); + } else if (gIndex == 0) { + throw ExceptionUtil.coreException("Failed to read memory value. Invalid memory: " + memoryStringVal); + } + + try { + return Integer.valueOf(memoryStringVal) * (gb ? 1024 : 1); + } catch (NumberFormatException e) { + throw ExceptionUtil.coreException("Failed to parse memory due to: " + e.getMessage()); + } + } + + /** + * + * @return map of parsed manifest file, if the file exists. If the file does + * not exist, return null. + * @throws CoreException + * if manifest file exists, but error occurred that prevents a + * map to be generated. + */ + @SuppressWarnings("unchecked") + protected Map parseManifestFromFile() throws Exception { + + InputStream inputStream = getInputStream(); + + if (inputStream != null) { + Yaml yaml = new Yaml(); + + try { + Object results = yaml.load(inputStream); + + if (results instanceof Map) { + return (Map) results; + } else { + String source = manifestFile == null ? "entered manifest" : "file " + manifestFile.getFullPath(); + throw ExceptionUtil.coreException("Expected a map of values for " + + source + ". Unable to load manifest content. Actual results: " + results); + } + + } finally { + try { + inputStream.close(); + } catch (IOException e) { + // Ignore + } + } + + } + return null; + } + + static protected String getMemoryAsString(int memory) { + if (memory < 1) { + return null; + } + return memory + "M"; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/ApplicationNameReconciler.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/ApplicationNameReconciler.java new file mode 100644 index 000000000..e17896480 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/ApplicationNameReconciler.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 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.cf.deployment; + +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.reconciler.Reconciler; +import org.eclipse.jface.text.source.ISourceViewer; +import org.springframework.ide.eclipse.editor.support.yaml.ast.YamlASTProvider; +import org.yaml.snakeyaml.Yaml; + +/** + * Reconciler for Application names in CF deployment manifest YAML + * + * @author Alex Boyko + * + */ +public class ApplicationNameReconciler extends Reconciler { + + private AppNameReconcilingStrategy strategy; + + public ApplicationNameReconciler() { + super(); + YamlASTProvider parser = new YamlASTProvider(new Yaml()); + strategy= new AppNameReconcilingStrategy(parser); + this.setReconcilingStrategy(strategy, IDocument.DEFAULT_CONTENT_TYPE); + } + + @Override + public void install(ITextViewer textViewer) { + super.install(textViewer); + if (textViewer instanceof ISourceViewer) { + strategy.install((ISourceViewer)textViewer); + } + } + + @Override + public void uninstall() { + super.uninstall(); + strategy.uninstall(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/CloudApplicationDeploymentProperties.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/CloudApplicationDeploymentProperties.java new file mode 100644 index 000000000..eee7aea5e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/CloudApplicationDeploymentProperties.java @@ -0,0 +1,329 @@ +/******************************************************************************* + * Copyright (c) 2015, 2018 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.cf.deployment; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFPushArguments; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFRoute; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeploymentProperties; +import org.springframework.ide.eclipse.boot.util.JavaProjectUtil; + +import com.google.common.collect.ImmutableList; + +public class CloudApplicationDeploymentProperties implements DeploymentProperties { + + private boolean enableJmxSshTunnel; + private List boundServices; + private Map environmentVariables; + private String buildpack; + private List buildpacks; + + private int instances; + + /* + * URLs should never be null. If no URLs are needed, keep list empty + */ + private LinkedHashSet urls; + + private String appName; + + public boolean getEnableJmxSshTunnel() { + return enableJmxSshTunnel; + } + + public void setEnableJmxSshTunnel(boolean enableJmxSshTunnel) { + this.enableJmxSshTunnel = enableJmxSshTunnel; + } + + private IProject project; + + private int memory; + + private int diskQuota; + + private IFile manifestFile; + + private Integer timeout; + + private String command; + + private String stack; + + /** + * Path to a zipFile containing the contents of the stuff to deploy. + */ + private File archive; + private String healthCheckType; + private String healthCheckHttpEndpoint; + private boolean randomeRoute = false; + + public CloudApplicationDeploymentProperties() { + boundServices = new ArrayList<>(); + environmentVariables = new HashMap<>(); + buildpack = ""; + buildpacks = new ArrayList<>(); + instances = DeploymentProperties.DEFAULT_INSTANCES; + urls = new LinkedHashSet<>(); + appName = null; + project = null; + memory = DeploymentProperties.DEFAULT_MEMORY; + diskQuota = DeploymentProperties.DEFAULT_MEMORY; + manifestFile = null; + timeout = null; + command = null; + stack = null; + } + + public void setProject(IProject project) { + this.project = project; + } + + public IProject getProject() { + return project; + } + + public void setMemory(int memory) { + this.memory = memory; + } + + public int getMemory() { + return memory; + } + + public void setDiskQuota(int diskQuota) { + this.diskQuota = diskQuota; + } + + public int getDiskQuota() { + return diskQuota; + } + + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public Integer getTimeout() { + return timeout; + } + + public String getHealthCheckType() { + return healthCheckType; + } + + public void setHealthCheckType(String healthCheckType) { + this.healthCheckType = healthCheckType; + } + + public String getHealthCheckHttpEndpoint() { + return healthCheckHttpEndpoint; + } + + public void setHealthCheckHttpEndpoint(String hche) { + this.healthCheckHttpEndpoint = hche; + } + + public void setCommand(String command) { + this.command = command; + } + + public String getCommand() { + return command; + } + + public void setStack(String stack) { + this.stack = stack; + } + + public String getStack() { + return stack; + } + + public void setManifestFile(IFile file) { + this.manifestFile = file; + } + + public IFile getManifestFile() { + return this.manifestFile; + } + + /** + * Returns a copy of the list of URLs for the application + * + * @return never null + */ + public Set getUris() { + return urls; + } + + public void setUris(Collection urls) { + this.urls = urls == null ? new LinkedHashSet<>() : new LinkedHashSet<>(urls); + } + + public void setAppName(String appName) { + this.appName = appName; + } + + public String getAppName() { + return this.appName; + } + + public void setBuildpack(String buildpack) { + this.buildpack = buildpack; + } + + public void setServices(List services) { + /* + * List should be read/write accessible hence create new instance rather + * than use emptyList from Collections if null is passed in + */ + boundServices = services == null ? new ArrayList<>() : services; + } + + public void setBuildpacks(List buildpacks) { + this.buildpacks = buildpacks == null ? new ArrayList<>() : buildpacks; + } + + public void setInstances(int instances) { + this.instances = instances; + } + + public void setRandomRoute(boolean randomRoute) { + this.randomeRoute = randomRoute; + } + + public boolean getRandomRoute() { + return this.randomeRoute ; + } + + public void setEnvironmentVariables(Map environmentVariables) { + /* + * Map should be read/write accessible hence create new instance rather + * than use emptyMap from Collections if null is passed in + */ + this.environmentVariables = environmentVariables == null ? new HashMap<>() : environmentVariables; + } + + public String getBuildpack() { + return buildpack == null || buildpack.isEmpty() ? null : buildpack; + } + + @Override + public List getBuildpacks() { + return ImmutableList.copyOf(buildpacks); + } + + public int getInstances() { + return instances; + } + + /** + * @return never null + */ + public Map getEnvironmentVariables() { + return environmentVariables; + } + + /** + * + * @return never null + */ + public List getServices() { + return boundServices; + } + + public static CloudApplicationDeploymentProperties getFor(IProject project, CloudData cloudData, CFApplication app) { + + CloudApplicationDeploymentProperties properties = new CloudApplicationDeploymentProperties(); + + properties.setAppName(app == null ? project.getName() : app.getName()); + properties.setProject(project); + properties.setBuildpack(app == null ? cloudData.getBuildpack() : app.getBuildpackUrl()); + + /* + * TODO: Re-evaluate whether JAVA_OPTS need to be treated differently + * Boot Dash Tooling adds staff to JAVA-OPTS behind the scenes. Consider + * JAVA_OPTS env variable as the one not exposed to users + */ + Map env = new LinkedHashMap<>(); + if (app != null) { + env.putAll(app.getEnvAsMap()); + env.remove("JAVA_OPTS"); + } + + // PT 174076433 - Support deploying of Java 11 apps to CF + if (JavaProjectUtil.isJava11(project)) { + env.put("JBP_CONFIG_OPEN_JDK_JRE", "{ jre: { version: 11.+}}"); + } + + properties.setEnvironmentVariables(env); + + properties.setInstances(app == null ? 1 : app.getInstances()); + properties.setMemory(app == null ? DeploymentProperties.DEFAULT_MEMORY : app.getMemory()); + properties.setServices(app == null ? Collections.emptyList() : app.getServices()); + properties.setDiskQuota(app == null ? DeploymentProperties.DEFAULT_MEMORY : app.getDiskQuota()); + properties.setTimeout(app == null ? null : app.getTimeout()); + properties.setHealthCheckType(app==null ? null : app.getHealthCheckType()); + properties.setHealthCheckHttpEndpoint(app == null ? null : app.getHealthCheckHttpEndpoint()); + properties.setCommand(app == null ? null : app.getCommand()); + properties.setStack(app == null ? null : app.getStack()); + + if (app == null) { + CFRoute route = CFRoute.builder().host(project.getName()).domain(cloudData.getDefaultDomain()).build(); + properties.setUris(Collections.singletonList(route.getRoute())); + } else { + properties.setUris(app.getUris()); + } + return properties; + } + + public CFPushArguments toPushArguments(List cloudDomains) throws Exception { + Set uris = getUris(); + CFPushArguments args = new CFPushArguments(); + args.setRoutes(uris); + args.setAppName(getAppName()); + args.setMemory(getMemory()); + args.setDiskQuota(getDiskQuota()); + args.setTimeout(getTimeout()); + args.setHealthCheckType(getHealthCheckType()); + args.setHealthCheckHttpEndpoint(getHealthCheckHttpEndpoint()); + args.setBuildpack(getBuildpack()); + args.setCommand(getCommand()); + args.setStack(getStack()); + args.setEnv(getEnvironmentVariables()); + args.setInstances(getInstances()); + args.setServices(getServices()); + args.setApplicationData(getArchive()); + args.setRandomRoute(getRandomRoute()); + return args; + } + + public File getArchive() { + return archive; + } + + public void setArchive(File archive) { + this.archive = archive; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/CloudData.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/CloudData.java new file mode 100644 index 000000000..8d07ec8d0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/CloudData.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.deployment; + +import java.util.List; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFDomainType; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFStack; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFDomainStatus; + +import com.google.common.collect.ImmutableList; + +public class CloudData { + + private List domains; + private String buildpack; + private List stacks; + + public CloudData(List domains, String buildpack, List stacks) { + this.domains = domains; + this.buildpack = buildpack; + this.stacks = stacks; + } + + public String getBuildpack() { + return buildpack; + } + + public List getDomains() { + if (domains!=null) { + return domains; + } + return ImmutableList.of(); + } + + public List getStacks() { + if (stacks!=null) { + return stacks; + } + return ImmutableList.of(); + } + + public String getDefaultDomain() { + return getDomains().stream() + .filter(d -> d.getStatus()==CFDomainStatus.SHARED && d.getType()==CFDomainType.HTTP) + .findFirst() + .map(d -> d.getName()) + .orElse(null); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/ManifestDiffDialog.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/ManifestDiffDialog.java new file mode 100644 index 000000000..2234be66d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/ManifestDiffDialog.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.deployment; + +import org.eclipse.compare.CompareEditorInput; +import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.dialogs.DialogSettings; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.dialogs.TitleAreaDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.dialogs.ManifestDiffDialogModel; +import org.springframework.ide.eclipse.boot.dash.dialogs.ManifestDiffDialogModel.Result; + +/** + * Dialog to compare and merge manifest file with deployment properties from CF. + * Hosts eclipse's compare and merge editor composite. + * + * @author Alex Boyko + * + */ +public class ManifestDiffDialog extends TitleAreaDialog { + + protected final CompareEditorInput fCompareEditorInput; + private String title = "Merge Manifest File"; + + /** + * Create a dialog to host the given input. + * @param shell a shell + * @param input the dialog input + */ + public ManifestDiffDialog(Shell shell, ManifestDiffDialogModel model) { + super(shell); + CompareEditorInput input = model.getInput(); + setShellStyle(getShellStyle() | SWT.RESIZE | SWT.MAX); + Assert.isNotNull(input); + fCompareEditorInput= input; + } + + public ManifestDiffDialog(Shell shell, ManifestDiffDialogModel model, String title) { + this(shell, model); + this.title = title; + } + + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.YES_ID, "Use Manifest", true); + createButton(parent, IDialogConstants.CANCEL_ID, fCompareEditorInput.getCancelButtonLabel(), false); + createButton(parent, IDialogConstants.NO_ID, "Forget Manifest", false); + } + + protected Control createDialogArea(Composite parent2) { + + Composite parent= (Composite) super.createDialogArea(parent2); + + Control c= fCompareEditorInput.createContents(parent); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Shell shell= c.getShell(); + shell.setText(fCompareEditorInput.getTitle()); + shell.setImage(fCompareEditorInput.getTitleImage()); + applyDialogFont(parent); + return parent; + } + + @Override + public void create() { + super.create(); + setTitle(title); + if (fCompareEditorInput != null && fCompareEditorInput.getMessage() != null) { + setMessage(fCompareEditorInput.getMessage(), IMessageProvider.WARNING); + } else { + setMessage( + "Manifest file deployment properties are different from current deployment properties on CF. Please merge changes if applicable.", + IMessageProvider.INFORMATION); + } + } + + protected void buttonPressed(int buttonId) { + switch (buttonId) { + case IDialogConstants.NO_ID: + fCompareEditorInput.cancelPressed(); + setReturnCode(buttonId); + close(); + break; + case IDialogConstants.YES_ID: + if (fCompareEditorInput.isDirty()) { + if (!fCompareEditorInput.okPressed()) + return; + } else { + fCompareEditorInput.cancelPressed(); + } + setReturnCode(buttonId); + close(); + break; + case IDialogConstants.CANCEL_ID: + fCompareEditorInput.cancelPressed(); + super.buttonPressed(buttonId); + break; + default: + super.buttonPressed(buttonId); + } + } + + protected IDialogSettings getDialogBoundsSettings() { + return DialogSettings.getOrCreateSection(BootDashActivator.getDefault().getDialogSettings(), "MergeManifestDialog"); + } + + /** + * Return the compare editor input for this dialog. + * @return the compare editor input for this dialog + */ + protected final CompareEditorInput getInput() { + return fCompareEditorInput; + } + + @Override + protected int getDialogBoundsStrategy() { + return DIALOG_PERSISTSIZE; + } + + public static Result getResultForCode(int buttonId) { + switch (buttonId) { + case IDialogConstants.NO_ID: + return Result.FORGET_MANIFEST; + case IDialogConstants.YES_ID: + return Result.USE_MANIFEST; + case IDialogConstants.CANCEL_ID: + return Result.CANCELED; + default: + throw new IllegalArgumentException("Unknown button ID"); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/UnsupportedPushProperties.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/UnsupportedPushProperties.java new file mode 100644 index 000000000..7c5db2a26 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/UnsupportedPushProperties.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2018 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.cf.deployment; + +import java.util.List; + +import org.eclipse.core.runtime.OperationCanceledException; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +import com.google.common.collect.ImmutableList; + +public class UnsupportedPushProperties { + + + + public UnsupportedPushProperties() { + } + + /** + * Check for unsupported push properties in the given deployment properties. Either allow the + * caller of this check to continue if unsupported properties are found, or + * cancel. If no unsupported properties are found, nothing happens. + * + * @param ui + * @param cde + * @param deploymentProperties + * @throws OperationCanceledException if operation is cancelled + */ + public void allowOrCancelIfFound(UserInteractions ui, + CloudApplicationDeploymentProperties deploymentProperties) throws OperationCanceledException { + List unsupportedProperties = findUnsupportedProperties(deploymentProperties); + if (!unsupportedProperties.isEmpty()) { + StringBuilder builder = new StringBuilder(); + builder.append( + "The following properties are not currently supported when pushing an application to Cloud Foundry. These properties will be ignored. Continue with deployment?"); + builder.append('\n'); + + for (String prop : unsupportedProperties) { + builder.append('\n'); + builder.append("- "); + builder.append(prop); + } + if (!ui.confirmOperation("Unsupported Push Properties", builder.toString())) { + throw new OperationCanceledException(); + } + } + } + + /** + * Check the given deployment properties for unsupported properties. Return + * properties found that are unsupported, or empty list if nothing is found. + * + * @param properties + * @return + */ + public List findUnsupportedProperties(CloudApplicationDeploymentProperties properties) { + if (properties != null) { + if (properties.getBuildpacks() != null && !properties.getBuildpacks().isEmpty()) { + return ImmutableList.of("buildpacks"); + } + } + return ImmutableList.of(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/YamlGraphDeploymentProperties.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/YamlGraphDeploymentProperties.java new file mode 100644 index 000000000..45a415e90 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/deployment/YamlGraphDeploymentProperties.java @@ -0,0 +1,1493 @@ +/******************************************************************************* + * Copyright (c) 2016, 2018 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.cf.deployment; + +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.text.edits.DeleteEdit; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFRoute; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeploymentProperties; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springframework.ide.eclipse.editor.support.reconcile.IProblemCollector; +import org.springframework.ide.eclipse.editor.support.yaml.ast.NodeMergeSupport; +import org.springframework.ide.eclipse.editor.support.yaml.ast.NodeUtil; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.DumperOptions.FlowStyle; +import org.yaml.snakeyaml.DumperOptions.LineBreak; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.composer.Composer; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.parser.ParserImpl; +import org.yaml.snakeyaml.reader.StreamReader; +import org.yaml.snakeyaml.resolver.Resolver; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * Deployment properties based on YAML Graph. Instance of this class has ability + * to compute text differences between this instance and deployment properties + * passed as parameter + * + * @author Alex Boyko + * + */ +public class YamlGraphDeploymentProperties implements DeploymentProperties { + + private String content; + private MappingNode appNode; + private Node root; + private SequenceNode applicationsValueNode; + private Yaml yaml; + private CloudData cloudData; + + public YamlGraphDeploymentProperties(String content, String appName, CloudData cloudData) { + super(); + this.appNode = null; + this.applicationsValueNode = null; + this.root = null; + this.cloudData = cloudData; + this.content = content; + initializeYaml(appName); + } + + private void initializeYaml(String appName) { + Composer composer = new Composer(new ParserImpl(new StreamReader(new InputStreamReader(new ByteArrayInputStream(content.getBytes())))), new Resolver()); + root = composer.getSingleNode(); + + NodeMergeSupport mergeSupport = new NodeMergeSupport(IProblemCollector.NULL); + mergeSupport.mergeAll(root); + + Node apps = YamlGraphDeploymentProperties.findValueNode(root, "applications"); + if (apps instanceof SequenceNode) { + applicationsValueNode = (SequenceNode) apps; + appNode = findAppNode(applicationsValueNode, appName); + } else if (root instanceof MappingNode) { + appNode = (MappingNode) root; + } + + this.yaml = new Yaml(createDumperOptions()); + } + + private static MappingNode findAppNode(SequenceNode seq, String name) { + if (name != null) { + for (Node n : seq.getValue()) { + Node nameValue = findValueNode(n, ApplicationManifestHandler.NAME_PROP); + if (nameValue instanceof ScalarNode && ((ScalarNode)nameValue).getValue().equals(name)) { + return (MappingNode) n; + } + } + } + return null; + } + + @Override + public String getYamlContent() { + return content; + } + + public static DumperOptions createDumperOptions() { + DumperOptions options = new DumperOptions(); + options.setExplicitStart(false); + options.setCanonical(false); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(FlowStyle.BLOCK); + options.setLineBreak(LineBreak.getPlatformLineBreak()); + return options; + } + + @SuppressWarnings("unchecked") + static public T getNode(Node node, String key, Class type) { + Node n = findValueNode(node, key); + if (n != null && type.isAssignableFrom(n.getClass())) { + return (T) n; + } + return null; + } + + @Override + public String getAppName() { + /* + * Name must be located in the app node! + */ + return getPropertyValue(appNode, ApplicationManifestHandler.NAME_PROP, String.class); + } + + @Override + public int getMemory() { + String memoryStringValue = getAbsoluteValue(ApplicationManifestHandler.MEMORY_PROP, String.class); + if (memoryStringValue != null) { + try { + return ApplicationManifestHandler.convertMemory(memoryStringValue); + } catch (CoreException e) { + Log.log(e); + } + } + return DeploymentProperties.DEFAULT_MEMORY; + } + + public String getInheritFilePath() { + return getPropertyValue(root, ApplicationManifestHandler.INHERIT_PROP, String.class); + } + + @Override + public String getBuildpack() { + return getAbsoluteValue(ApplicationManifestHandler.BUILDPACK_PROP, String.class); + } + + @SuppressWarnings("unchecked") + @Override + public Map getEnvironmentVariables() { + Map map = getAbsoluteValue(ApplicationManifestHandler.ENV_PROP, Map.class); + return map == null ? Collections.emptyMap() : map; + } + + @Override + public int getInstances() { + Integer n = getAbsoluteValue(ApplicationManifestHandler.INSTANCES_PROP, Integer.class); + return n == null ? DeploymentProperties.DEFAULT_INSTANCES : n.intValue(); + } + + @Override + public Integer getTimeout() { + return getAbsoluteValue(ApplicationManifestHandler.TIMEOUT_PROP, Integer.class); + } + + @Override + public String getCommand() { + return getAbsoluteValue(ApplicationManifestHandler.COMMAND_PROP, String.class); + } + + @Override + public String getHealthCheckType() { + return getAbsoluteValue(ApplicationManifestHandler.HEALTH_CHECK_TYPE_PROP, String.class); + } + + @Override + public String getHealthCheckHttpEndpoint() { + return getAbsoluteValue(ApplicationManifestHandler.HEALTH_CHECK_HTTP_ENDPOINT_PROP, String.class); + } + + @Override + public String getStack() { + return getAbsoluteValue(ApplicationManifestHandler.STACK_PROP, String.class); + } + + @SuppressWarnings("unchecked") + @Override + public List getServices() { + List services = getAbsoluteValue(ApplicationManifestHandler.SERVICES_PROP, List.class); + return services == null ? Collections.emptyList() : services; + } + + public static Node findValueNode(Node node, String key) { + return NodeUtil.getProperty(node, key); + } + + public static NodeTuple findNodeTuple(MappingNode mapping, String key) { + if (mapping != null) { + for (NodeTuple tuple : mapping.getValue()) { + if (tuple.getKeyNode() instanceof ScalarNode) { + ScalarNode scalar = (ScalarNode) tuple.getKeyNode(); + if (key.equals(scalar.getValue())) { + return tuple; + } + } + } + } + return null; + } + + private ReplaceEdit addLineBreakIfMissing(int index) { + int i = index - 1; + for (; i >= 0 && Character.isWhitespace(content.charAt(i)) && content.charAt(i) != '\n'; i--); + if (i > 0 && content.charAt(i) != '\n') { + return new ReplaceEdit(index, 0, System.lineSeparator()); + } + return null; + } + + public MultiTextEdit getDifferences(DeploymentProperties props) { + MultiTextEdit edits = new MultiTextEdit(); + TextEdit edit; + + if (appNode == null) { + Map obj = ApplicationManifestHandler.toYaml(props, cloudData, isLegacyHostDomainManifestYaml(root)); + if (applicationsValueNode == null) { + DumperOptions options = new DumperOptions(); + options.setExplicitStart(true); + options.setCanonical(false); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(FlowStyle.BLOCK); + options.setLineBreak(LineBreak.getPlatformLineBreak()); + edits.addChild(new ReplaceEdit(0, content.length(), new Yaml(options).dump(obj))); + } else { + edit = addLineBreakIfMissing(applicationsValueNode.getEndMark().getIndex()); + if (edit != null) { + edits.addChild(edit); + } + @SuppressWarnings("unchecked") + /* + * Find the appropriate application Object in the list. + */ + List appsObj = (List) obj.get(ApplicationManifestHandler.APPLICATIONS_PROP); + Object appObject = appsObj.get(0); + for (Object entry : appsObj) { + if (entry instanceof Map && Objects.equal(props.getAppName(), ((Map)entry).get(ApplicationManifestHandler.NAME_PROP))) { + appObject = entry; + break; + } + } + edits.addChild(new ReplaceEdit(applicationsValueNode.getEndMark().getIndex(), 0, serializeListEntry(appObject, applicationsValueNode.getStartMark().getColumn()).toString())); + } + } else { + if (!Objects.equal(getAppName(), props.getAppName())) { + edit = createEdit(appNode, props.getAppName(), ApplicationManifestHandler.NAME_PROP); + if (edit != null) { + edits.addChild(edit); + } + } + + /* + * Compare value because strings may have 'G', 'M' etc post-fixes + */ + if (getMemory() != props.getMemory()) { + edit = createEdit(appNode, String.valueOf(props.getMemory()) + "M", ApplicationManifestHandler.MEMORY_PROP); + if (edit != null) { + edits.addChild(edit); + } + } + + if (getInstances() != props.getInstances()) { + getDifferenceForEntry(edits, ApplicationManifestHandler.INSTANCES_PROP, props.getInstances(), DEFAULT_INSTANCES, Integer.class); + } + + if (!Objects.equal(getTimeout(), props.getTimeout())) { + getDifferenceForEntry(edits, ApplicationManifestHandler.TIMEOUT_PROP, props.getTimeout(), null, Integer.class); + } + + if (!Objects.equal(getHealthCheckType(), props.getHealthCheckType())) { + getDifferenceForEntry(edits, ApplicationManifestHandler.HEALTH_CHECK_TYPE_PROP, props.getHealthCheckType(), + DeploymentProperties.DEFAULT_HEALTH_CHECK_TYPE, String.class); + } + + if (!Objects.equal(getHealthCheckHttpEndpoint(), props.getHealthCheckHttpEndpoint())) { + getDifferenceForEntry(edits, ApplicationManifestHandler.HEALTH_CHECK_HTTP_ENDPOINT_PROP, props.getHealthCheckHttpEndpoint(), + DeploymentProperties.DEFAULT_HEALTH_CHECK_HTTP_ENDPOINT, String.class); + } + + if (!Objects.equal(getCommand(), props.getCommand())) { + getDifferenceForEntry(edits, ApplicationManifestHandler.COMMAND_PROP, props.getCommand(), null, String.class); + } + + /* + * Only if 'stack' attribute is present in the manifest perform the comparison + */ + if (getStack() != null && !getStack().equals(props.getStack())) { + getDifferenceForEntry(edits, ApplicationManifestHandler.STACK_PROP, props.getStack(), null, String.class); + } + + if (getDiskQuota() != props.getDiskQuota()) { + edit = createEdit(appNode, String.valueOf(props.getDiskQuota()) + "M", ApplicationManifestHandler.DISK_QUOTA_PROP); + if (edit != null) { + edits.addChild(edit); + } + } + + if (!Objects.equal(getBuildpack(), props.getBuildpack())) { + edit = createEdit(appNode, props.getBuildpack(), ApplicationManifestHandler.BUILDPACK_PROP); + if (edit != null) { + edits.addChild(edit); + } + } + + if (!new HashSet<>(getServices()).equals(new HashSet<>(props.getServices()))) { + getDifferencesForList(edits, ApplicationManifestHandler.SERVICES_PROP, props.getServices(), String.class); + } + + if (!getEnvironmentVariables().equals(props.getEnvironmentVariables())) { + getDifferencesForMap(edits, ApplicationManifestHandler.ENV_PROP, props.getEnvironmentVariables()); + } + + /* + * If any text edits are produced then there are differences in the URIs + */ + Set currentUris = getUris(); + Set otherUris = props.getUris(); + if (!isRandomRouteMatch(currentUris, otherUris) && !currentUris.equals(otherUris)) { + if (isLegacyHostDomainManifestYaml(root)) { + getLegacyDifferenceForUris(otherUris, edits); + } else { + getDifferenceForUris(otherUris, edits); + } + } + } + return edits.hasChildren() ? edits : null; + } + + private boolean isRandomRouteMatch(Set currentUris, Set otherUris) { + if (currentUris.size() == 1 && otherUris.size() == 1) { + String uri = currentUris.iterator().next(); + String host = uri.substring(0, uri.indexOf('.')); + return ApplicationManifestHandler.RANDOM_VAR.equals(host); + } + return false; + } + + /** + * Creates diff text edits for entry in the map node given by the + * attribute's name based on its new value and type as well as the default + * value that is not serialized under normal circumstances + * + * @param me + * container to append text edits + * @param key + * manifest attribute name + * @param newValue + * the new value to create diff edits for + * @param defaultValue + * the default value for the attribute that is usually not + * serialized + * @param type + * type of the value for the attribute + */ + private void getDifferenceForEntry(MultiTextEdit me, String key, T newValue, T defaultValue, Class type) { + TextEdit edit = null; + if (Objects.equal(newValue, defaultValue)) { + /* + * New value is the default value. Check if entry can be safely removed from YAML + */ + T rootValue = getPropertyValue(root, key, type); + if (appNode != root && rootValue != null) { + if (newValue == null) { + me.addChild(createEdit((MappingNode)root, (Object) null, key)); + for (Node n : applicationsValueNode.getValue()) { + if (n instanceof MappingNode) { + MappingNode application = (MappingNode) n; + if (application == appNode) { + edit = createEdit(appNode, (Object) null, key); + if (edit != null) { + me.addChild(edit); + } + } else { + T appValue = getPropertyValue(application, key, type); + if (appValue == null) { + me.addChild(createEdit(application, rootValue, key)); + } + } + } + } + } else { + edit = createEdit(appNode, newValue, key); + if (edit != null) { + me.addChild(edit); + } + } + } else { + edit = createEdit(appNode, (T) null, key); + if (edit != null) { + me.addChild(edit); + } + } + } else { + /* + * New value is not default hence it have to be serialized and + * therefore would override the value in the root node for the same + * attribute + */ + edit = createEdit(appNode, newValue, key); + if (edit != null) { + me.addChild(edit); + } + } + } + + /** + * Creates text edits based on differences between current manifest + * attribute list value and the passed new list value. The manifest + * attribute value is considered to be defined either on the application or + * the root node of the manifest YAML and text edit is calculated + * accordingly. It also supports multiple apps defined in the manifest + * + * @param me + * multi text edit gathering all edits + * @param key + * manifest attribute name + * @param newValue + * the new value to set + */ + @SuppressWarnings("unchecked") + private void getDifferencesForList(MultiTextEdit me, String key, List newValue, Class type) { + TextEdit edit; + /* + * Moved new value entries in the set to avoid duplication + */ + LinkedHashSet otherValue = new LinkedHashSet<>(newValue); + /* + * Get the list value from the root node + */ + List rootList = root != appNode ? getPropertyValue(root, key, List.class) : Collections.emptyList(); + if (rootList == null) { + rootList = Collections.emptyList(); + } + if (otherValue.containsAll(rootList)) { + /* + * All list entries from the root are present in the new value + */ + otherValue.removeAll(rootList); + /* + * Create an edit for a difference between the remaining list of + * values and current app's node list + */ + edit = createEdit(appNode, new ArrayList<>(otherValue), key, type); + if (edit != null) { + me.addChild(edit); + } + } else { + /* + * Some list entries from the root are missing move all root values + * to application nodes. Applications value node must be present + * because rootList wasn't empty since we got here + */ + for (Node n : applicationsValueNode.getValue()) { + if (n instanceof MappingNode) { + MappingNode application = (MappingNode) n; + if (n == appNode) { + /* + * Current app node + */ + edit = createEdit(application, newValue, key, type); + if (edit != null) { + me.addChild(edit); + } + } else { + /* + * Any other app node. Get its list value for the attribute + */ + List currentValues = getPropertyValue(application, key, List.class); + if (currentValues == null) { + /* + * There is no value for the property so just create an edit for the list from the root + */ + edit = createEdit(application, rootList, key, type); + } else { + /* + * Create a joint list of values from app's node list value and root node list value + */ + LinkedHashSet values = new LinkedHashSet<>(currentValues); + values.addAll(rootList); + /* + * Create and edit with the new value being the joint list + */ + edit = createEdit(application, new ArrayList<>(values), key, type); + } + if (edit != null) { + me.addChild(edit); + } + } + } + } + /* + * Remove the list from the root node + */ + edit = createEdit((MappingNode)root, (Object)null, key); + if (edit != null) { + me.addChild(edit); + } + } + } + + /** + * Creates text edits based on differences between current manifest + * attribute map value and the passed new map value. The manifest + * attribute value is considered to be defined either on the application or + * the root node of the manifest YAML and text edit is calculated + * accordingly. It also supports multiple apps defined in the manifest + * + * @param me + * multi text edit gathering all edits + * @param key + * manifest attribute name + * @param newValue + * the new value to set + */ + @SuppressWarnings("unchecked") + private void getDifferencesForMap(MultiTextEdit me, String key, Map newValue) { + TextEdit edit; + /* + * Get the map value from the root node + */ + Map rootMap = root != appNode ? getPropertyValue(root, key, Map.class) : Collections.emptyMap(); + /* + * Copy the new value to leave it unchanged + */ + LinkedHashMap otherValue = new LinkedHashMap<>(newValue); + if (rootMap == null) { + rootMap = Collections.emptyMap(); + } + if (otherValue.keySet().containsAll(rootMap.keySet())) { + /* + * All map entries from the root are present in the new value + */ + for (String k : rootMap.keySet()) { + if (Objects.equal(otherValue.get(k), rootMap.get(k))) { + otherValue.remove(k); + } + } + /* + * Create an edit for a difference between the remaining map and + * current app's node map + */ + edit = createEdit(appNode, otherValue, key); + if (edit != null) { + me.addChild(edit); + } + } else { + /* + * Some map entries from the root must be removed. Move root node map to applications. + * Applications value node must be present because rootList wasn't empty since we got here. + */ + for (Node n : applicationsValueNode.getValue()) { + if (n instanceof MappingNode) { + MappingNode application = (MappingNode) n; + if (n == appNode) { + /* + * Current app node + */ + edit = createEdit(application, newValue, key); + if (edit != null) { + me.addChild(edit); + } + } else { + /* + * Any other app node. Get its map value for the attribute + */ + Map currentValues = getPropertyValue(application, key, Map.class); + if (currentValues == null) { + /* + * There is no value for the property so just create an edit for the map from the root + */ + edit = createEdit(application, rootMap, key); + } else { + /* + * Create a joint map of entries from app's node map value and root node map value + */ + for (Map.Entry entry : rootMap.entrySet()) { + if (!currentValues.containsKey(entry.getKey())) { + currentValues.put(entry.getKey(), entry.getValue()); + } + } + /* + * Create and edit with the new value being the joint map + */ + edit = createEdit(application, currentValues, key); + } + if (edit != null) { + me.addChild(edit); + } + } + } + } + /* + * Remove the list from the root node + */ + edit = createEdit((MappingNode)root, Collections.emptyMap(), key); + if (edit != null) { + me.addChild(edit); + } + + } + } + + private void getDifferenceForUris(Collection uris, MultiTextEdit me) { + Boolean randomRoute = getAbsoluteValue(ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class); + Boolean noRoute = getAbsoluteValue(ApplicationManifestHandler.NO_ROUTE_PROP, Boolean.class); + boolean otherNoRoute = uris.isEmpty(); + boolean match = false; + + if (otherNoRoute) { + if (!Boolean.TRUE.equals(noRoute)) { + getDifferenceForEntry(me, ApplicationManifestHandler.NO_ROUTE_PROP, true, false, Boolean.class); + + if (getPropertyValue(appNode, ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class) != null) { + me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.RANDOM_ROUTE_PROP)); + } + } else { + match = true; + } + } else { + if (Boolean.TRUE.equals(noRoute)) { + getDifferenceForEntry(me, ApplicationManifestHandler.NO_ROUTE_PROP, false, false, Boolean.class); + } + + if (Boolean.TRUE.equals(randomRoute) && uris.size() == 1 && getAbsoluteValue(ApplicationManifestHandler.ROUTES_PROP, Map.class) == null) { + match = true; + } else if (getPropertyValue(appNode, ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class) != null) { + me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.RANDOM_ROUTE_PROP)); + } + } + + if (!match) { + getDifferencesForList(me, ApplicationManifestHandler.ROUTES_PROP, uris.stream().map(uri -> { + Map routeObj = new LinkedHashMap<>(); + routeObj.put(ApplicationManifestHandler.ROUTE_PROP, uri); + return routeObj; + }).collect(Collectors.toList()), Map.class); + } + + } + + private void getLegacyDifferenceForUris(Collection uris, MultiTextEdit me) { + List domains = cloudData.getDomains(); + + LinkedHashSet otherHosts = new LinkedHashSet<>(); + LinkedHashSet otherDomains = new LinkedHashSet<>(); + ApplicationManifestHandler.extractHostsAndDomains(uris, domains, otherHosts, otherDomains); + boolean otherNoRoute = otherHosts.isEmpty() && otherDomains.isEmpty(); + boolean otherNoHostname = otherHosts.isEmpty() && !otherDomains.isEmpty(); + + LinkedHashSet currentHosts = new LinkedHashSet<>(); + LinkedHashSet currentDomains = new LinkedHashSet<>(); + + /* + * Gather hosts from "host" and "hosts" attributes from app and root nodes + */ + String host = getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAIN_PROP, String.class); + if (host != null) { + currentHosts.add(host); + } + List hostsList = getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAINS_PROP, List.class); + if (hostsList != null) { + for (Object o : hostsList) { + if (o instanceof String) { + currentHosts.add((String) o); + } + } + } + + /* + * Gather domains from 'domain' and 'domains' attributes from app and root nodes + */ + String domain = getAbsoluteValue(ApplicationManifestHandler.DOMAIN_PROP, String.class); + if (domain != null) { + currentDomains.add(domain); + } + List domainsList = getAbsoluteValue(ApplicationManifestHandler.DOMAINS_PROP, List.class); + if (domainsList != null) { + for (Object o : domainsList) { + if (o instanceof String) { + currentDomains.add((String) o); + } + } + } + + boolean match = false; + Boolean noHost = getAbsoluteValue(ApplicationManifestHandler.NO_HOSTNAME_PROP, Boolean.class); + Boolean randomRoute = getAbsoluteValue(ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class); + Boolean noRoute = getAbsoluteValue(ApplicationManifestHandler.NO_ROUTE_PROP, Boolean.class); + + if (otherNoRoute) { + if (!Boolean.TRUE.equals(noRoute)) { + getDifferenceForEntry(me, ApplicationManifestHandler.NO_ROUTE_PROP, true, false, Boolean.class); + + if (getPropertyValue(appNode, ApplicationManifestHandler.NO_HOSTNAME_PROP, Boolean.class) != null) { + me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.NO_HOSTNAME_PROP)); + } + + if (getPropertyValue(appNode, ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class) != null) { + me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.RANDOM_ROUTE_PROP)); + } + } else { + match = true; + } + } else { + if (Boolean.TRUE.equals(noRoute)) { + getDifferenceForEntry(me, ApplicationManifestHandler.NO_ROUTE_PROP, false, false, Boolean.class); + } + + if (otherNoHostname) { + if (!Boolean.TRUE.equals(noHost)) { + me.addChild(createEdit(appNode, Boolean.TRUE, ApplicationManifestHandler.NO_HOSTNAME_PROP)); + } + } else { + /* + * There is at least a host in the deployment properties. Remove + * "no-hostname" attribute if there is one from the application + * node. Don't care if it's in the root or anywhere else + */ + if (getPropertyValue(appNode, ApplicationManifestHandler.NO_HOSTNAME_PROP, Boolean.class) != null) { + me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.NO_HOSTNAME_PROP)); + } + } + + if (Boolean.TRUE.equals(randomRoute) && otherHosts.size() == 1 && otherDomains.size() == 1 && currentHosts.isEmpty()) { + match = true; + } else if (getPropertyValue(appNode, ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class) != null) { + me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.RANDOM_ROUTE_PROP)); + } + + if (currentHosts.isEmpty() && !Boolean.TRUE.equals(noHost)) { + currentHosts.add(getAppName()); + } + + if (currentDomains.isEmpty() && !domains.isEmpty()) { + currentDomains.add(cloudData.getDefaultDomain()); + } + } + + if (!match && (!currentHosts.equals(otherHosts) || !currentDomains.equals(otherDomains))) { + generateEditForHostsAndDomains(me, currentHosts, currentDomains, otherHosts, otherDomains); + } + + } + + private void generateEditForHostsAndDomains(MultiTextEdit me, Set currentHosts, Set currentDomains, Set otherHosts, Set otherDomains) { + /* + * Calculate current 'host' attrbute value + */ + String host = getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAIN_PROP, String.class); + if (otherHosts.size() == 1) { + /* + * Only one host for deployment props + */ + String otherHost = otherHosts.iterator().next(); + /* + * If calculated host is different from deployment props create edit + */ + if (host == null || !otherHost.equals(host)) { + getDifferenceForEntry(me, ApplicationManifestHandler.SUB_DOMAIN_PROP, otherHost, null, String.class); + } + /* + * Ensure the deployment props hosts are empty since the difference has been dealt with here + */ + otherHosts.clear(); + } else { + /* + * Deployment props have more than one host. + * Check if current "host" attribute value is one of the hosts from deployment props + */ + if (host != null && !otherHosts.remove(host)) { + /* + * If current 'host' attribute value is not contained in + * deployment props hosts then ensure "host" attribute value is + * cleared + */ + getDifferenceForEntry(me, ApplicationManifestHandler.SUB_DOMAIN_PROP, null, null, String.class); + } + } + /* + * Calculate edit for hosts list + */ + getDifferencesForList(me, ApplicationManifestHandler.SUB_DOMAINS_PROP, new ArrayList<>(otherHosts), String.class); + + /* + * Calculate current 'domain' attribute value + */ + String domain = getAbsoluteValue(ApplicationManifestHandler.DOMAIN_PROP, String.class); + if (otherDomains.size() == 1) { + /* + * Only one domain for deployment props + */ + String otherDomain = otherDomains.iterator().next(); + /* + * If calculated domain is different from deployment props create edit + */ + if (domain == null || !otherDomain.equals(domain)) { + getDifferenceForEntry(me, ApplicationManifestHandler.DOMAIN_PROP, otherDomain, null, String.class); + } + /* + * Ensure the deployment props domains are empty since the difference has been dealt with here + */ + otherDomains.clear(); + } else { + /* + * Deployment props have more than one domain. + * Check if current "domain" attribute value is one of the domains from deployment props + */ + if (domain != null && !otherDomains.remove(domain)) { + /* + * If current 'domain' attribute value is not contained in + * deployment props domains then ensure "domain" attribute value is + * cleared + */ + getDifferenceForEntry(me, ApplicationManifestHandler.DOMAIN_PROP, null, null, String.class); + } + } + /* + * Calculate edit for domains list + */ + getDifferencesForList(me, ApplicationManifestHandler.DOMAINS_PROP, new ArrayList<>(otherDomains), String.class); + } + + /** + * Creates text edit for mapping node tuples where property and value are + * scalars (i.e. value is either string or some primitive type) + * + * @param parent + * the parent MappingNode + * @param otherValue + * the new value for the tuple + * @param property + * tuple's key + * @return the text edit + */ + private TextEdit createEdit(MappingNode parent, Object otherValue, String property) { + NodeTuple tuple = findNodeTuple(parent, property); + if (tuple == null) { + if (otherValue != null) { + StringBuilder serializedValue = serialize(property, otherValue); + boolean[] postIndent = new boolean[] { true }; + int position = positionToAppendAt(parent, postIndent); + if (postIndent[0]) { + postIndent(serializedValue, getDefaultOffset()); + } else { + preIndent(serializedValue, getDefaultOffset()); + } + return new ReplaceEdit(position, 0, serializedValue.toString()); + } + } else { + if (otherValue == null) { + /* + * Delete the tuple including the line break if possible + */ + int start = tuple.getKeyNode().getStartMark().getIndex(); + int end = tuple.getValueNode().getEndMark().getIndex(); + /* + * k1: v1 + * k-delete: v-delete + * ^ ^ + * start index end index + * k2: v2 + * + * Extend end index to position of k2 and leave start index where it was with correct indent + * + * However, if it' the last tuple in the map than just delete it and leave the line with the indent in the beginning for now. + */ + if (parent.getValue().get(parent.getValue().size() - 1) != tuple) { + for (; end > 0 && end < content.length() && Character.isWhitespace(content.charAt(end)); end++); + } + return new DeleteEdit(start, end - start); + } else { + /* + * Replace the current value (whether it's a scalr value or anything else without affecting the white space + */ + return new ReplaceEdit(tuple.getValueNode().getStartMark().getIndex(), tuple.getValueNode().getEndMark().getIndex() - tuple.getValueNode().getStartMark().getIndex(), String.valueOf(otherValue)); +// return createReplaceEditWithoutWhiteSpace(tuple.getValueNode().getStartMark().getIndex(), tuple.getValueNode().getEndMark().getIndex() - 1, +// String.valueOf(otherValue)); + } + } + return null; + } + + /** + * Calculates position to append entries to the map node. Also provides a + * hint on how to properly append entries. If entries are to be appended + * after the last entry in the map node then they need to be all + * pre-indented, and post-indented otherwise + * + * @param m the map node + * @param postIndent the post- or pre- indent calculated hint + * @return the index to append entries + */ + private int positionToAppendAt(MappingNode m, boolean[] postIndent) { + /* + * Check if there is a name attribute in the map node (case of application node) and make the end index of tha 'name: XXX' tuple as the index to append + */ + for (NodeTuple tuple : m.getValue()) { + if (tuple.getKeyNode() instanceof ScalarNode + && ApplicationManifestHandler.NAME_PROP.equals(((ScalarNode) tuple.getKeyNode()).getValue())) { + int index = tuple.getValueNode().getEndMark().getIndex(); + for (; index > 0 && index < content.length() && Character.isWhitespace(content.charAt(index)); index++) + ; + postIndent[0] = m.getValue().get(m.getValue().size() - 1) != tuple; + return index; + } + } + postIndent[0] = true; + return m.getStartMark().getIndex(); + } + + private TextEdit createEdit(MappingNode parent, List otherValue, String property, Class type) { + NodeTuple tuple = findNodeTuple(parent, property); + if (tuple == null) { + if (otherValue != null && !otherValue.isEmpty()) { + StringBuilder serializedValue = serialize(property, otherValue); +// postIndent(serializedValue, getDefaultOffset()); +// int position = positionToAppendAt(parent); + boolean[] postIndent = new boolean[] { true }; + int position = positionToAppendAt(parent, postIndent); + if (postIndent[0]) { + postIndent(serializedValue, getDefaultOffset()); + } else { + preIndent(serializedValue, getDefaultOffset()); + } + return new ReplaceEdit(position, 0, serializedValue.toString()); + } + } else { + if (otherValue == null || otherValue.isEmpty()) { + int start = tuple.getKeyNode().getStartMark().getIndex(); + int end = tuple.getValueNode().getEndMark().getIndex(); +// int index = parent.getValue().indexOf(tuple); +// if (!(index > 0 && parent.getValue().get(index - 1).getValueNode() instanceof CollectionNode)) { +// /* +// * If previous tuple is not a map or list then try to remove the preceding line break +// */ +// for (; start > 0 && Character.isWhitespace(content.charAt(start - 1)) && content.charAt(start - 1) != '\n'; start--); +// } + for (; end > 0 && end < content.length() && Character.isWhitespace(content.charAt(end)); end++); + + return new DeleteEdit(start, end - start); + } else { + Node sequence = tuple.getKeyNode(); + if (tuple.getValueNode() instanceof SequenceNode) { + SequenceNode sequenceValue = (SequenceNode) tuple.getValueNode(); + MultiTextEdit me = new MultiTextEdit(); + Set others = new LinkedHashSet<>(); + others.addAll(otherValue); + + /* + * Remember the ending position of the last entry that remains in the list + */ + int appendIndex = sequenceValue.getEndMark().getIndex(); + for (int index = 0; index < sequenceValue.getValue().size(); index++) { + Node n = sequenceValue.getValue().get(index); + T value = getValue(n, type); + if (others.contains(value)) { + // Entry exists, do nothing, just update the end position to append the missing entries + others.remove(value); + appendIndex = n.getEndMark().getIndex(); + } else { + /* + * skip "- " prefix for the start position + */ + int start = n.getStartMark().getIndex(); + for (; start > 0 && content.charAt(start) != '-' && content.charAt(start) != '\n'; start--); + int end = n.getEndMark().getIndex(); + + // If entry is object in the list don't remove the indent for the next entry in YAML (if there is a next YAML entry) + /* + * - e: entry-1 + * ^-start + * - e: entry-2 + * ^-end + */ + if (n instanceof MappingNode && root.getEndMark().getIndex() != end) { + if (parent.getEndMark().getIndex() == n.getEndMark().getIndex()) { + // last entry in the list but not the last yaml piece in the document + end -= (parent.getStartMark().getColumn() - appNode.getStartMark().getColumn()); + } else { + end -= sequenceValue.getStartMark().getColumn(); + } + } + /* + * "- entry" start=2, end=7, need to include '\n' in the deletion + */ + DeleteEdit deleteEdit = createDeleteEditIncludingLine(start, end); + appendIndex = deleteEdit.getOffset(); + me.addChild(deleteEdit); + } + } + /* + * TODO: verify that further appendIndex manipulations are necessary! + */ + /* + * Offset appendIndex to leave the line break for the previous entry in place. jump over spacing and line break. + */ + for (; appendIndex > 0 && appendIndex < content.length() && Character.isWhitespace(content.charAt(appendIndex)) && content.charAt(appendIndex - 1) != '\n'; appendIndex++); + /* + * Add a line break if append index is not starting right after line break. + */ + if (!others.isEmpty() && content.charAt(appendIndex - 1) != '\n') { + me.addChild(new ReplaceEdit(appendIndex, 0, System.lineSeparator())); + } + + /* + * Add missing entries + */ + for (T s : others) { + me.addChild(new ReplaceEdit(appendIndex, 0, serializeListEntry(s, sequenceValue.getStartMark().getColumn()).toString())); + } + return me.hasChildren() ? me : null; + } else { + /* + * Sequence is expected but was something else. Replace the + * whole tuple. Don't touch the whitespace when replacing - + * it looks good + */ + StringBuilder s = serialize(property, otherValue); + preIndent(s, sequence.getStartMark().getColumn()); + return createReplaceEditWithoutWhiteSpace(sequence.getStartMark().getIndex(), + tuple.getValueNode().getEndMark().getIndex() - 1, + s.toString().trim()); + } + } + } + return null; + } + + private TextEdit createEdit(MappingNode parent, Map otherValue, String property) { + NodeTuple tuple = findNodeTuple(parent, property); + if (tuple == null) { + /* + * No tuple found for the key + */ + if (otherValue != null && !otherValue.isEmpty()) { + /* + * If other value is something that can be serialized, serialize the key and other value and put in the YAML + */ + StringBuilder serializedValue = serialize(property, otherValue); +// postIndent(serializedValue, getDefaultOffset()); +// int position = positionToAppendAt(parent); + boolean[] postIndent = new boolean[] { true }; + int position = positionToAppendAt(parent, postIndent); + if (postIndent[0]) { + postIndent(serializedValue, getDefaultOffset()); + } else { + preIndent(serializedValue, getDefaultOffset()); + } + return new ReplaceEdit(position, 0, serializedValue.toString()); + } + } else { + /* + * Tuple with the string key is found + */ + if (otherValue == null || otherValue.isEmpty()) { + /* + * Delete the found tuple since other value is null or empty + */ + int start = tuple.getKeyNode().getStartMark().getIndex(); + int end = tuple.getValueNode().getEndMark().getIndex(); + return new DeleteEdit(start, end - start); +// return createDeleteEditIncludingLine(tuple.getKeyNode().getStartMark().getIndex(), tuple.getValueNode().getEndMark().getIndex()); + } else { + /* + * Tuple is found, so the key node is there, check the value node + */ + Node map = tuple.getKeyNode(); + if (tuple.getValueNode() instanceof MappingNode) { + /* + * Value node is a map node. Go over every entry in the map to calculate differences + */ + MappingNode mapValue = (MappingNode) tuple.getValueNode(); + MultiTextEdit e = new MultiTextEdit(); + Map leftOver = new LinkedHashMap<>(); + leftOver.putAll(otherValue); + int appendIndex = mapValue.getStartMark().getIndex(); + for (NodeTuple t : mapValue.getValue()) { + if (t.getKeyNode() instanceof ScalarNode && t.getValueNode() instanceof ScalarNode) { + ScalarNode key = (ScalarNode) t.getKeyNode(); + ScalarNode value = (ScalarNode) t.getValueNode(); + String newValue = leftOver.get(key.getValue()); + if (newValue == null) { + /* + * Delete the tuple if newValue is null. Delete including the line if necessary + */ + e.addChild(createDeleteEditIncludingLine(key.getStartMark().getIndex(), value.getEndMark().getIndex())); + } else if (!value.getValue().equals(newValue)) { + /* + * Key is there but value is different, so edit the value + */ + e.addChild(new ReplaceEdit(value.getStartMark().getIndex(), value.getEndMark().getIndex() - value.getStartMark().getIndex(), newValue)); + appendIndex = value.getEndMark().getIndex(); + } else { + appendIndex = value.getEndMark().getIndex(); + } + leftOver.remove(key.getValue()); + } + } + /* + * Offset appendIndex to leave the line break for the previous entry in place. jump over spacing and line break. + */ + for (; appendIndex > 0 && appendIndex < content.length() && Character.isWhitespace(content.charAt(appendIndex)) && content.charAt(appendIndex - 1) != '\n'; appendIndex++); + /* + * Add a line break if append index is not starting right after line break. + */ + if (!leftOver.isEmpty() && content.charAt(appendIndex - 1) != '\n') { + e.addChild(new ReplaceEdit(appendIndex, 0, System.lineSeparator())); + } + /* + * Add remaining unmatched entries + */ + for (Map.Entry entry : leftOver.entrySet()) { + StringBuilder serializedValue = serialize(entry.getKey(), entry.getValue()); + preIndent(serializedValue, mapValue.getStartMark().getColumn()); + e.addChild(new ReplaceEdit(appendIndex, 0, serializedValue.toString())); + } + return e.hasChildren() ? e : null; + } else { + /* + * Map is expected but was something else. Replace the + * whole tuple. Don't touch the whitespace when replacing - + * it looks good + */ + StringBuilder serializedValue = serialize(property, otherValue); + preIndent(serializedValue, map.getStartMark().getColumn()); + return createReplaceEditWithoutWhiteSpace(map.getStartMark().getIndex(), tuple.getValueNode().getEndMark().getIndex() - 1, serializedValue.toString().trim()); + } + } + } + return null; + } + + private StringBuilder serialize(String property, Object value) { + Map obj = new HashMap<>(); + obj.put(property, value); + return new StringBuilder(yaml.dump(obj)); + } + + private StringBuilder postIndent(StringBuilder s, int offset) { + char[] indent = new char[offset]; + for (int i = 0; i < offset; i++) { + indent[i] = ' '; + } + for (int i = 0; i < s.length(); ) { + if (s.charAt(i) == '\n') { + s.insert(i + 1, indent); + i += indent.length; + } + i++; + } + return s; + } + + private StringBuilder serializeListEntry(Object obj, int offset) { + StringBuilder s = new StringBuilder(yaml.dump(Collections.singletonList(obj))); + if (offset > 0) { + preIndent(s, offset); + } + return s; + } + + private StringBuilder preIndent(StringBuilder s, int offset) { + char[] indent = new char[offset]; + for (int i = 0; i < offset; i++) { + indent[i] = ' '; + } + int lineLength = 0; + for (int i = 0; i < s.length(); ) { + if (s.charAt(i) == '\n') { + if (lineLength > 0) { + s.insert(i - lineLength, indent); + i += indent.length; + lineLength = 0; + } + } else { + lineLength++; + } + i++; + } + if (lineLength > 0) { + s.insert(s.length() - lineLength, indent); + lineLength = 0; + } + return s; + } + + private DeleteEdit createDeleteEditIncludingLine(int start, int end) { + if (content != null) { + for (; start > 0 && Character.isWhitespace(content.charAt(start - 1)) && content.charAt(start - 1) != '\n'; start--); + for (; end > 0 && end < content.length() && Character.isWhitespace(content.charAt(end)) && content.charAt(end - 1) != '\n'; end++); + } + return new DeleteEdit(start, end - start); + } + + private ReplaceEdit createReplaceEditWithoutWhiteSpace(int start, int end, String text) { + for (; start < content.length() && Character.isWhitespace(content.charAt(start)); start++); + for (; end >= start && Character.isWhitespace(content.charAt(end)); end--); + return new ReplaceEdit(start, end - start + 1, text); + } + + private int getDefaultOffset() { + if (appNode == null) { + if (applicationsValueNode == null) { + return 0; + } else { + return applicationsValueNode.getStartMark().getColumn(); + } + } else { + return appNode.getStartMark().getColumn(); + } + } + + public List getRoutes() { + List> routes = getAbsoluteValue(ApplicationManifestHandler.ROUTES_PROP, List.class); + if (routes != null) { + return routes.stream() + .map(routeObj -> routeObj.get(ApplicationManifestHandler.ROUTE_PROP)) + .filter(o -> o instanceof String) + .map(o -> (String) o) + .collect(Collectors.toList()); + } + return null; + } + + @Override + @SuppressWarnings("unchecked") + public Set getUris() { + + List routes = getRoutes(); + if (routes != null) { + return ImmutableSet.copyOf(routes); + } else { + Boolean noRoute = getAbsoluteValue(ApplicationManifestHandler.NO_ROUTE_PROP, Boolean.class); + if (Boolean.TRUE.equals(noRoute)) { + return Collections.emptySet(); + } + + List domains = cloudData.getDomains(); + LinkedHashSet hostsSet = new LinkedHashSet<>(); + LinkedHashSet domainsSet = new LinkedHashSet<>(); + + /* + * Gather domains from app node from 'domain' and 'domains' attributes + */ + String domain = getAbsoluteValue(ApplicationManifestHandler.DOMAIN_PROP, String.class); + if (domain != null) { + domainsSet.add(domain); + } + List domainsList = getAbsoluteValue(ApplicationManifestHandler.DOMAINS_PROP, List.class); + if (domainsList != null) { + domainsSet.addAll(domainsList); + } + + /* + * Gather hosts from app node from 'host' and 'hosts' + * attributes. + */ + String host = getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAIN_PROP, String.class); + if (host != null) { + hostsSet.add(host); + } + List hostsList = getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAINS_PROP, List.class); + if (hostsList != null) { + for (Object o : hostsList) { + if (o instanceof String) { + hostsSet.add((String)o); + } + } + } + + /* + * If no host names found check for "random-route: true" and + * "no-hostname: true" otherwise take app name as the host name + */ + if (hostsSet.isEmpty()) { + Boolean randomRoute = getAbsoluteValue(ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class); + if (Boolean.TRUE.equals(randomRoute)) { + hostsSet.add(ApplicationManifestHandler.RANDOM_VAR); + domainsSet.clear(); + domainsSet.add(cloudData.getDefaultDomain()); + } else { + Boolean noHostname = getAbsoluteValue(ApplicationManifestHandler.NO_HOSTNAME_PROP, Boolean.class); + if (!Boolean.TRUE.equals(noHostname)) { + hostsSet.add(getAppName()); + } + } + } + + /* + * Set a domain if they are still empty + */ + if (domainsSet.isEmpty()) { + domainsSet.add(cloudData.getDefaultDomain()); + } + + /* + * Compose URIs for application based on hosts and domains + */ + Set uris = new HashSet<>(); + for (String d : domainsSet) { + if (hostsSet.isEmpty()) { + uris.add(CFRoute.builder().domain(d).build().getRoute()); + } else { + for (String h : hostsSet) { + uris.add(CFRoute.builder().host(h).domain(d).build().getRoute()); + } + } + } + + return uris; + } + } + + @Override + public int getDiskQuota() { + String quotaStringValue = getAbsoluteValue(ApplicationManifestHandler.DISK_QUOTA_PROP, String.class); + if (quotaStringValue != null) { + try { + return ApplicationManifestHandler.convertMemory(quotaStringValue); + } catch (CoreException e) { + Log.log(e); + } + } + return DeploymentProperties.DEFAULT_MEMORY; + } + + public static V getPropertyValue(final Node n, final String key, Class parameter) { + Node node = YamlGraphDeploymentProperties.findValueNode(n, key); + return getValue(node, parameter); + } + + @SuppressWarnings("unchecked") + public static V getValue(Node node, Class parameter) { + return (V) new Constructor(parameter) { + @Override + public Object getSingleData(Class type) { + // Ensure that the stream contains a single document and construct it + if (node != null) { + if (type != null) { + node.setTag(new Tag(type)); + } else { + node.setTag(rootTag); + } + return constructObject(node); + } + return null; + } + }.getSingleData(parameter); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected V getAbsoluteValue(String key, Class parameter) { + V v = getPropertyValue(appNode, key, parameter); + if (Collection.class.isAssignableFrom(parameter)) { + if (root != appNode) { + V rootV = getPropertyValue(root, key, parameter); + if (rootV != null) { + if (v != null){ + ((Collection) rootV).addAll((Collection) v); + } + v = rootV; + } + } + } else if (Map.class.isAssignableFrom(parameter)) { + if (root != appNode) { + V rootV = getPropertyValue(root, key, parameter); + if (rootV != null) { + if (v != null){ + ((Map) rootV).putAll((Map) v); + } + v = rootV; + } + } + } else if (v == null) { + if (root != appNode) { + v = getPropertyValue(root, key, parameter); + } + } + return v; + } + + private static boolean isLegacyHostDomainManifestYaml(Node n) { + if (isLegacyHostDomainManifestYamlNode(n)) { + return true; + } else { + Node applicationsObj = findValueNode(n, ApplicationManifestHandler.APPLICATIONS_PROP); + if (applicationsObj instanceof SequenceNode) { + return ((SequenceNode) applicationsObj).getValue().stream() + .map(o -> (Node) o) + .filter(YamlGraphDeploymentProperties::isLegacyHostDomainManifestYamlNode) + .findFirst().isPresent(); + } + } + return false; + } + + private static boolean isLegacyHostDomainManifestYamlNode(Node node) { + return findValueNode(node, ApplicationManifestHandler.DOMAIN_PROP) != null + || findValueNode(node, ApplicationManifestHandler.SUB_DOMAIN_PROP) != null + || findValueNode(node, ApplicationManifestHandler.DOMAINS_PROP) != null + || findValueNode(node, ApplicationManifestHandler.SUB_DOMAINS_PROP) != null + || findValueNode(node, ApplicationManifestHandler.NO_HOSTNAME_PROP) != null; + } + + public String getRawHost() { + return getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAIN_PROP, String.class); + } + public List getRawHosts() { + List hostsList = getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAINS_PROP, List.class); + if (hostsList != null) { + List currentHosts = new ArrayList<>(); + for (Object o : hostsList) { + if (o instanceof String) { + currentHosts.add((String) o); + } + } + return ImmutableList.copyOf(currentHosts); + } + return null; + } + + public List getRawDomains() { + List hostsList = getAbsoluteValue(ApplicationManifestHandler.DOMAINS_PROP, List.class); + if (hostsList != null) { + List currentHosts = new ArrayList<>(); + for (Object o : hostsList) { + if (o instanceof String) { + currentHosts.add((String) o); + } + } + return ImmutableList.copyOf(currentHosts); + } + return null; + } + + public boolean getRawNoRoute() { + Boolean v = getAbsoluteValue(ApplicationManifestHandler.NO_ROUTE_PROP, Boolean.class); + return v==null ? false : v; + } + + public boolean getRawRandomRoute() { + Boolean v = getAbsoluteValue(ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class); + return v==null ? false : v; + } + + public String getRawDomain() { + return getAbsoluteValue(ApplicationManifestHandler.DOMAIN_PROP, String.class); + } + + public boolean getRawNoHost() { + Boolean v = getAbsoluteValue(ApplicationManifestHandler.NO_HOSTNAME_PROP, Boolean.class); + return v==null ? false : v; + } + + @SuppressWarnings("unchecked") + @Override + public List getBuildpacks() { + List buildpacks = getAbsoluteValue(ApplicationManifestHandler.BUILDPACKS_PROP, List.class); + return buildpacks == null ? new ArrayList<>() : buildpacks; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CloudFoundryTargetWizardModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CloudFoundryTargetWizardModel.java new file mode 100644 index 000000000..3ed74a561 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CloudFoundryTargetWizardModel.java @@ -0,0 +1,479 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.dialogs; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.equinox.security.storage.StorageException; +import org.eclipse.jface.operation.IRunnableContext; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials.LoginMethod; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFExceptions; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFSpace; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.CloudFoundryClientFactory; +import org.springframework.ide.eclipse.boot.dash.cf.ops.Operation; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryTargetProperties; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModelContext; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.WizardModelUserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.CannotAccessPropertyException; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.TargetProperties; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.livexp.core.CompositeValidator; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +import com.google.common.collect.ImmutableSet; + +/** + * Cloud Foundry Target properties that uses {@link LiveExpression} and + * {@link Validator}. + */ +public class CloudFoundryTargetWizardModel { + + private RunTargetType runTargetType; + private BootDashModelContext context; + + private final LiveVariable url = new LiveVariable<>(); + private final LiveVariable space = new LiveVariable<>(); + private final LiveVariable selfsigned = new LiveVariable<>(false); + private final LiveVariable skipSslValidation = new LiveVariable<>(false); + private final LiveVariable method = new LiveVariable<>(LoginMethod.PASSWORD); + private final LiveVariable userName = new LiveVariable<>(); + private final LiveVariable password = new LiveVariable<>(); + private final LiveVariable storeCredentials = new LiveVariable<>(StoreCredentialsMode.STORE_NOTHING); + + private final LiveVariable spaceResolutionStatus = new LiveVariable<>(ValidationResult.OK); // has an error if resolution failed. + private final LiveVariable resolvedSpaces = new LiveVariable<>(); + + private String refreshToken = null; + + private final Validator credentialsValidator = new Validator() { + { + dependsOn(url); + dependsOn(userName); + dependsOn(method); + dependsOn(password); + } + + @Override + protected ValidationResult compute() { + if (isEmpty(userName.getValue()) && method.getValue()==LoginMethod.PASSWORD) { + return ValidationResult.info("Enter a username"); + } else if (isEmpty(url.getValue())) { + try { + new URL(url.getValue()); + return ValidationResult.info("Enter a target URL"); + } catch (MalformedURLException e) { + return ValidationResult.error(e.getMessage()); + } + } else if (method.getValue()==LoginMethod.PASSWORD) { + if (isEmpty(password.getValue())) { + return ValidationResult.info("Enter a password"); + } + } else if (method.getValue()==LoginMethod.TEMPORARY_CODE) { + if (isEmpty(password.getValue())) { + return ValidationResult.info("Enter a Temporary Access Code"); + } + } + return ValidationResult.OK; + } + + protected boolean isEmpty(String value) { + return value == null || value.trim().length() == 0; + } + }; + + private final Validator spaceValidator = new Validator() { + { + dependsOn(space); + } + @Override + protected ValidationResult compute() { + if (getSpaceName() == null || getOrganizationName() == null) { + return ValidationResult.info("Select a Cloud space"); + } + + if (space.getValue() != null) { + RunTarget existing = CloudFoundryTargetWizardModel.this.getExistingRunTarget(space.getValue()); + if (existing != null) { + return ValidationResult.error("A run target for that space already exists: '" + existing.getName() + + "'. Please select another space."); + } + } + return ValidationResult.OK; + } + }; + private Validator resolvedSpacesValidator = new Validator() { + { + dependsOn(spaceResolutionStatus); + dependsOn(resolvedSpaces); + } + @Override + protected ValidationResult compute() { + ValidationResult resolveStatus = spaceResolutionStatus.getValue(); + if (!resolveStatus.isOk()) { + return resolveStatus; + } + if (resolvedSpaces.getValue() == null || resolvedSpaces.getValue().getAllSpaces() == null) { + return ValidationResult.info("Select a space to validate the credentials."); + } + if (resolvedSpaces.getValue().getAllSpaces().isEmpty()) { + return ValidationResult.error( + "No spaces available to select. Please check that the credentials and target URL are correct, and spaces are defined in the target."); + } + return ValidationResult.OK; + } + }; + private Validator storeCredentialsValidator = PasswordDialogModel.makeStoreCredentialsValidator(method, storeCredentials); + private CompositeValidator allPropertiesValidator = new CompositeValidator(); + + private CloudFoundryClientFactory clientFactory; + private ImmutableSet existingTargets; + private WizardModelUserInteractions interactions; + + + public CloudFoundryTargetWizardModel(RunTargetType runTargetType, CloudFoundryClientFactory clientFactory, + ImmutableSet existingTargets, BootDashModelContext context) { + this(runTargetType, clientFactory, existingTargets, context, null); + } + + public CloudFoundryTargetWizardModel(RunTargetType runTargetType, CloudFoundryClientFactory clientFactory, + ImmutableSet existingTargets, BootDashModelContext context, WizardModelUserInteractions interactions) { + this.runTargetType = runTargetType; + this.context = context; + Assert.isNotNull(clientFactory, "clientFactory should not be null"); + this.interactions = interactions; + this.existingTargets = existingTargets == null ? ImmutableSet.of() : existingTargets; + this.clientFactory = clientFactory; + + // Aggregate of the credentials and space validators. + allPropertiesValidator.addChild(credentialsValidator); + allPropertiesValidator.addChild(storeCredentialsValidator); + allPropertiesValidator.addChild(resolvedSpacesValidator); + allPropertiesValidator.addChild(spaceValidator); + + url.setValue(getDefaultTargetUrl()); + } + + public void setUrl(String url) { + this.url.setValue(url); + } + + public void setSelfsigned(boolean selfsigned) { + this.selfsigned.setValue(selfsigned); + } + + public void skipSslValidation(boolean skipSsl) { + this.skipSslValidation.setValue(skipSsl); + } + + public void setUsername(String userName) { + this.userName.setValue(userName); + } + + public void setPassword(String password) throws CannotAccessPropertyException { + this.password.setValue(password); + } + + public void setSpace(CFSpace space) { + this.space.setValue(space); + } + + public String getPassword() throws CannotAccessPropertyException { + return password.getValue(); + } + + public String getUrl() { + return url.getValue(); + } + + public String getUsername() { + return userName.getValue(); + } + + public void setStoreCredentials(StoreCredentialsMode store) { + storeCredentials.setValue(store); + } + + public StoreCredentialsMode getStoreCredentials() { + return storeCredentials.getValue(); + } + + protected String getDefaultTargetUrl() { + return "https://api.run.pivotal.io"; + } + + public OrgsAndSpaces resolveSpaces(IRunnableContext context) { + try { + boolean toFetchSpaces = true; + OrgsAndSpaces spaces = getCloudSpaces(createTargetProperties(toFetchSpaces), context); + resolvedSpaces.setValue(spaces); + spaceResolutionStatus.setValue(ValidationResult.OK); + return resolvedSpaces.getValue(); + } catch (Exception e) { + if (CFExceptions.isAuthFailure(e) || CFExceptions.isSSLCertificateFailure(e)) { + //don't log, its expected if user just typed bad password, + //or didn't check ssl box when they should have. + } else { + Log.log(e); + } + resolvedSpaces.setValue(null); + spaceResolutionStatus.setValue(ValidationResult.error(ExceptionUtil.getMessage(e))); + return null; + } + } + + private OrgsAndSpaces getCloudSpaces(final CloudFoundryTargetProperties targetProperties, IRunnableContext context) + throws Exception { + + OrgsAndSpaces spaces = null; + + Operation> op = new Operation>( + "Connecting to the Cloud Foundry target. Please wait while the list of spaces is resolved...") { + @Override + protected List runOp(IProgressMonitor monitor) throws Exception, OperationCanceledException { + ClientRequests client = clientFactory.getClient(targetProperties); + try { + List spaces = client.getSpaces(); + String t = client.getRefreshToken(); + if (t!=null) { + refreshToken = t; + } + String effectiveUser = getUserName(client); + if (effectiveUser!=null) { + userName.setValue(effectiveUser); + } + return spaces; + } finally { + client.close(); + } + } + + private String getUserName(ClientRequests client) { + try { + return client.getUserName().block(); + } catch (Exception e) { + Log.log(e); + } + return null; + } + }; + + List actualSpaces = op.run(context, true); + if (actualSpaces != null && !actualSpaces.isEmpty()) { + spaces = new OrgsAndSpaces(actualSpaces); + } + + return spaces; + } + + /** + * Create target properties based on current input values in the wizard. + *

+ * Note that there are two slightly different ways to produce these properties. + *

+ * a) to create a intermediate client just to fetch orgs and spaces. + *

+ * b) the final properties used to create the client after space is selected and the user + * clicks 'finish' button. + */ + private CloudFoundryTargetProperties createTargetProperties(boolean toFetchSpaces) throws CannotAccessPropertyException { + CloudFoundryTargetProperties targetProps = new CloudFoundryTargetProperties(null, runTargetType, context.injections); + if (!toFetchSpaces) { + //Take care: when fetching spaces the space may not be known yet, so neither is the id + String id = CloudFoundryTargetProperties.getId( + this.getUsername(), + this.getUrl(), + this.getOrganizationName(), + this.getSpaceName() + ); + targetProps.put(TargetProperties.RUN_TARGET_ID, id); + } + + targetProps.setUrl(url.getValue()); + targetProps.setSelfSigned(selfsigned.getValue()); + targetProps.setSkipSslValidation(skipSslValidation.getValue()); + + targetProps.setUserName(userName.getValue()); + if (toFetchSpaces) { + targetProps.setStoreCredentials(StoreCredentialsMode.STORE_NOTHING); + targetProps.setCredentials(CFCredentials.fromLogin(method.getValue(), password.getValue())); + } else { + //use credentials of a style that is consistent with the 'store mode'. + if (method.getValue()==LoginMethod.TEMPORARY_CODE && storeCredentials.getValue()==StoreCredentialsMode.STORE_PASSWORD) { + //The temporary token shouldn't be stored since its meaningless. Silently downgrade storemode: + storeCredentials.setValue(StoreCredentialsMode.STORE_NOTHING); + } + StoreCredentialsMode mode = storeCredentials.getValue(); + targetProps.setStoreCredentials(storeCredentials.getValue()); + switch (mode) { + case STORE_NOTHING: + case STORE_TOKEN: + Assert.isTrue(refreshToken!=null); + targetProps.setCredentials(CFCredentials.fromRefreshToken(refreshToken)); + break; + case STORE_PASSWORD: + targetProps.setCredentials(CFCredentials.fromPassword(password.getValue())); + break; + default: + throw new IllegalStateException("BUG: Missing switch case?"); + } + } + targetProps.setSpace(space.getValue()); + return targetProps; + } + + public OrgsAndSpaces getSpaces() { + return resolvedSpaces.getValue(); + } + + protected RunTarget getExistingRunTarget(CFSpace space) { + if (space != null) { + String targetId = CloudFoundryTargetProperties.getId(getUsername(), getUrl(), + space.getOrganization().getName(), space.getName()); + for (RunTarget target : existingTargets) { + if (targetId.equals(target.getId())) { + return target; + } + } + } + return null; + } + + public CloudFoundryRunTarget finish() throws Exception { + CloudFoundryTargetProperties targetProps = null; + try { + targetProps = createTargetProperties(/*toFetchSpaces*/false); + } catch (Exception e) { + final StorageException storageException = getStorageException(e); + // Allow run target to be created on storage exceptions as the run target can still be created and connected + if (storageException != null) { + Log.log(storageException); + if (interactions != null) { + String message = "Failed to store credentials in secure storage. Please check your secure storage preferences. Error: " + + storageException.getMessage(); + interactions.informationPopup("Secure Storage Error", message); + } + storeCredentials.setValue(StoreCredentialsMode.STORE_NOTHING); + targetProps = createTargetProperties(/*toFetchSpaces*/false); + } else { + throw e; + } + } + return (CloudFoundryRunTarget) runTargetType.createRunTarget(targetProps); + } + + public String getSpaceName() { + CFSpace space = this.space.getValue(); + if (space!=null) { + return space.getName(); + } + return null; + } + + public String getOrganizationName() { + CFSpace space = this.space.getValue(); + if (space!=null) { + return space.getOrganization().getName(); + } + return null; + } + + protected StorageException getStorageException(Exception e) { + if (e instanceof StorageException) { + return (StorageException) e; + } + if (e.getCause() instanceof StorageException) { + return (StorageException) e.getCause(); + } + return null; + } + + /** + * @return A 'complete' validator that reflects the validation state of all the inputs in this 'ui'. + */ + public LiveExpression getValidator() { + return allPropertiesValidator; + } + + public String getRefreshToken() { + return refreshToken; + } + + public LiveVariable getMethodVar() { + return method; + } + + public LiveVariable getUserNameVar() { + return userName; + } + + public LiveVariable getPasswordVar() { + return password; + } + + public LiveVariable getStoreVar() { + return storeCredentials; + } + + public LiveVariable getSkipSslVar() { + return skipSslValidation; + } + + public LiveVariable getUrlVar() { + return url; + } + + public LiveVariable getSpaceVar() { + return space; + } + + public LiveExpression getEnableSpacesUI() { + return credentialsValidator.apply((r) -> r.isOk()); + } + + public LiveExpression getEnableUserName() { + return method.apply((method) -> method==LoginMethod.PASSWORD); + } + + public LiveExpression getSpaceValidator() { + return spaceValidator; + } + + public LiveExpression getResolvedSpacesValidator() { + return resolvedSpacesValidator; + } + + public LiveExpression getCredentialsValidator() { + return credentialsValidator; + } + + public void setMethod(LoginMethod v) { + method.setValue(v); + } + + public Validator getStoreCredentialsValidator() { + return storeCredentialsValidator; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CloudFoundryTargetWizardPage.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CloudFoundryTargetWizardPage.java new file mode 100644 index 000000000..cb1ec651c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CloudFoundryTargetWizardPage.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.dialogs; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials.LoginMethod; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.editor.support.util.CollectionUtil; +import org.springsource.ide.eclipse.commons.livexp.core.CompositeValidator; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.ui.CheckboxSection; +import org.springsource.ide.eclipse.commons.livexp.ui.ChooseOneSectionCombo; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.StringFieldSection; +import org.springsource.ide.eclipse.commons.livexp.ui.UIConstants; +import org.springsource.ide.eclipse.commons.livexp.ui.ValidatorSection; +import org.springsource.ide.eclipse.commons.livexp.ui.WizardPageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.WizardPageWithSections; + +/** + * Creates a Cloud Foundry target by prompting user for credentials and Cloud + * Foundry target URL. + * + * + */ +public class CloudFoundryTargetWizardPage extends WizardPageWithSections { + + private CloudFoundryTargetWizardModel model; + + private class SelectSpaceSection extends WizardPageSection { + + private CompositeValidator spaceSectionValidator = new CompositeValidator(); + { + //Do these two really need to be exposed from the model as separate entities? + // I don't think they are really used separately? + // Keeping it like this for now as it sort of make sense. The ui here has two pieces + // one is a box showing a selected space. And the other a button to resolve spaces. + spaceSectionValidator.addChild(model.getResolvedSpacesValidator()); + spaceSectionValidator.addChild(model.getSpaceValidator()); + } + + public SelectSpaceSection(IPageWithSections owner) { + super(owner); + } + + @Override + public void createContents(Composite page) { + Composite buttonComposite = new Composite(page, SWT.NONE); + GridDataFactory.fillDefaults().grab(true, false).applyTo(buttonComposite); + GridLayout layout = GridLayoutFactory.fillDefaults().numColumns(3).margins(0,2).create(); + buttonComposite.setLayout(layout); + + Label label = new Label(buttonComposite, SWT.NONE); + label.setText("Space:"); + GridDataFactory.fillDefaults() + .hint(UIConstants.fieldLabelWidthHint(label), SWT.DEFAULT) + .align(SWT.BEGINNING, SWT.CENTER) + .applyTo(label); + + Text spaceValueText = new Text(buttonComposite, SWT.BORDER); + spaceValueText.setEnabled(false); + spaceValueText.setBackground(buttonComposite.getBackground()); + GridDataFactory.fillDefaults().grab(true, false).applyTo(spaceValueText); + model.getSpaceVar().addListener((exp, value) -> { + if (spaceValueText != null && !spaceValueText.isDisposed()) { + spaceValueText.setText(value != null ? value.getName() : ""); + } + }); + + Button selectSpaceButton = new Button(buttonComposite, SWT.PUSH); + selectSpaceButton.setLayoutData(new GridData(SWT.END, SWT.CENTER, false, false)); + selectSpaceButton.setText("Select Space..."); + selectSpaceButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + + // Fetch an updated list of orgs and spaces in a cancellable + // operation (i.e. the operation + // can be cancelled in the wizard's progress bar) + model.resolveSpaces(getWizard().getContainer()); + OrgsAndSpaces spaces = model.getSpaces(); + if (spaces != null && CollectionUtil.hasElements(spaces.getAllSpaces())) { + OrgsAndSpacesWizard spacesWizard = new OrgsAndSpacesWizard(model); + WizardDialog dialog = new WizardDialog(getShell(), spacesWizard); + dialog.open(); + } + } + }); + + //Enable the "Select Space" button once credentials are complete. + model.getCredentialsValidator().addListener((exp, value) -> { + if (selectSpaceButton != null && !selectSpaceButton.isDisposed()) { + selectSpaceButton.setEnabled(value.isOk()); + } + }); + } + + @Override + public LiveExpression getValidator() { + return spaceSectionValidator; + } + + } + + + private CloudFoundryRunTarget runTarget = null; + + public CloudFoundryTargetWizardPage(CloudFoundryTargetWizardModel model) { + super("page1", "Add a Cloud Foundry Target", BootDashActivator.getImageDescriptor("icons/wizban_cloudfoundry.png")); + this.model = model; + setDescription("Enter credentials and a Cloud Foundry target URL."); + } + + @Override + protected List createSections() { + List sections = new ArrayList<>(); + sections.add(new ChooseOneSectionCombo<>(this, "Method:", model.getMethodVar(), EnumSet.allOf(LoginMethod.class))); + //TODO: hide password or passcode field depending on the method. + sections.add(new StringFieldSection(this, "Email:", model.getUserNameVar()) + .setEnabler(model.getEnableUserName())); + + sections.add(new StringFieldSection(this, "Password:", model.getPasswordVar()).setPassword(true)); + sections.add(UpdatePasswordDialog.storeCredentialsSection(this, model.getStoreVar(), model.getStoreCredentialsValidator())); + sections.add(new StringFieldSection(this, "Url:", model.getUrlVar())); + sections.add(new ValidatorSection(model.getCredentialsValidator(), this)); + sections.add(new SelectSpaceSection(this)); + sections.add(new CheckboxSection(this, model.getSkipSslVar(), "Skip SSL Validation")); + return sections; + } + + /** + * Creates a run target ONCE. + * @return created run target. Returns cached target if already created. + */ + public CloudFoundryRunTarget createRunTarget() { + // Cache to avoid creating run target multiple times in the same wizard session + if (runTarget == null) { + try { + runTarget = model.finish(); + } catch (Exception e) { + setErrorMessage(e.getMessage()); + } + } + return runTarget; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CustomizeAppsManagerURLDialog.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CustomizeAppsManagerURLDialog.java new file mode 100644 index 000000000..005dc355b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CustomizeAppsManagerURLDialog.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.dialogs; + +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.swt.widgets.Shell; +import org.springsource.ide.eclipse.commons.livexp.ui.ButtonSection; +import org.springsource.ide.eclipse.commons.livexp.ui.DialogWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.StringFieldSection; +import org.springsource.ide.eclipse.commons.livexp.ui.WizardPageSection; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +/** + * @author Martin Lippert + */ +public class CustomizeAppsManagerURLDialog extends DialogWithSections { + + private CustomizeAppsManagerURLDialogModel model; + + public CustomizeAppsManagerURLDialog(CustomizeAppsManagerURLDialogModel model, Shell shell) { + super("Customize Host URL of Apps Manager", model, shell); + this.model = model; + } + + @Override + protected List createSections() throws CoreException { + Builder sections = ImmutableList.builder(); + sections.add(new StringFieldSection(this, model.host).tooltip(model.getHelpText())); + sections.add(new ButtonSection(this, "Restore Defaults", model.restoreDefaultsHandler)); + return sections.build(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CustomizeAppsManagerURLDialogModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CustomizeAppsManagerURLDialogModel.java new file mode 100644 index 000000000..85a32ca88 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/CustomizeAppsManagerURLDialogModel.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.dialogs; + +import java.util.concurrent.Callable; + +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springsource.ide.eclipse.commons.livexp.core.StringFieldModel; +import org.springsource.ide.eclipse.commons.livexp.ui.OkButtonHandler; + +/** + * @author Martin Lippert + */ +public class CustomizeAppsManagerURLDialogModel implements OkButtonHandler { + + public final StringFieldModel host = new StringFieldModel("Apps Manager", ""); + private final CloudFoundryBootDashModel cloudFoundrySection; + + public CustomizeAppsManagerURLDialogModel(CloudFoundryBootDashModel cloudFoundrySection) { + this.cloudFoundrySection = cloudFoundrySection; + host.getVariable().setValue(this.cloudFoundrySection.getRunTarget().getAppsManagerHost()); + } + + public Callable restoreDefaultsHandler = new Callable() { + public Void call() throws Exception { + host.getVariable().setValue(getDefaultValue()); + return null; + } + }; + + @Override + public void performOk() throws Exception { + cloudFoundrySection.getRunTarget().setAppsManagerHost(host.getValue()); + cloudFoundrySection.notifyModelStateChanged(); + } + + public String getHelpText() { + return "no help available"; + } + + public String getDefaultValue() { + return cloudFoundrySection.getRunTarget().getAppsManagerHostDefault(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/DeploymentPropertiesDialog.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/DeploymentPropertiesDialog.java new file mode 100644 index 000000000..59483f88a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/DeploymentPropertiesDialog.java @@ -0,0 +1,705 @@ +/******************************************************************************* + * Copyright (c) 2016, 2020 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.cf.dialogs; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +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.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jface.dialogs.DialogSettings; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.jface.dialogs.TitleAreaDialog; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.jface.text.source.projection.ProjectionViewer; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITreeSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerFilter; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Sash; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IWorkbenchCommandConstants; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.handlers.IHandlerActivation; +import org.eclipse.ui.handlers.IHandlerService; +import org.eclipse.ui.model.BaseWorkbenchContentProvider; +import org.eclipse.ui.model.WorkbenchLabelProvider; +import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialogModel.ManifestType; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.UIValueListener; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; +import org.springsource.ide.eclipse.commons.livexp.ui.util.SwtConnect; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * Cloud Foundry Application deployment properties dialog. Allows user to select + * manifest YAML file or enter deployment manifest YAML manually. + * + * @author Alex Boyko + * + */ +public class DeploymentPropertiesDialog extends TitleAreaDialog { + + public static final String CONTEXT_DEPLOYMENT_PROPERTIES_DIALOG = "deployment-properties-dialog"; + + final static private String DIALOG_LIST_HEIGHT_SETTING = "ManifestFileDialog.listHeight"; //$NON-NLS-1$ + final static private String YML_EXTENSION = "yml"; //$NON-NLS-1$ + final static private String[] FILE_FILTER_NAMES = new String[] {"Manifest YAML files - *manifest*.yml", "YAML files - *.yml", "All files - *.*"}; + + + private static abstract class DeepFileFilter extends ViewerFilter { + + @Override + public boolean select(Viewer viewer, Object parent, Object element) { + if (element instanceof IResource && !((IResource)element).isDerived()) { + if (element instanceof IFile) { + return acceptFile((IFile)element); + } + if (element instanceof IContainer) { + try { + IContainer container = (IContainer) element; + for (IResource resource : container.members()) { + boolean select = select(viewer, container, resource); + if (select) { + return true; + } + } + } catch (CoreException e) { + // ignore + } + } + } + return false; + } + + abstract protected boolean acceptFile(IFile file); + + } + + final static private ViewerFilter YAML_FILE_FILTER = new DeepFileFilter() { + @Override + protected boolean acceptFile(IFile file) { + return YML_EXTENSION.equals(file.getFileExtension()); + } + }; + final static private ViewerFilter MANIFEST_YAML_FILE_FILTER = new DeepFileFilter() { + @Override + protected boolean acceptFile(IFile file) { + return file.getName().toLowerCase().contains("manifest") && YML_EXTENSION.equals(file.getFileExtension()); + } + }; + final static private ViewerFilter ALL_FILES = new ViewerFilter() { + @Override + public boolean select(Viewer viewer, Object parent, Object element) { + return (element instanceof IResource) && !((IResource)element).isDerived(); + } + }; + final static private ViewerFilter[][] RESOURCE_FILTERS = new ViewerFilter[][] { + {MANIFEST_YAML_FILE_FILTER}, + {YAML_FILE_FILTER}, + {ALL_FILES} + }; + + final static private int DEFAULT_WORKSPACE_GROUP_HEIGHT = 200; + + private Label fileLabel; + private Sash resizeSash; + private TreeViewer workspaceViewer; + private Button refreshButton; + private Button buttonFileManifest; + private Button buttonManualManifest; + private Group fileGroup; + private Group yamlGroup; + private Composite fileYamlComposite; + private Composite manualYamlComposite; + private Combo fileFilterCombo; + private IHandlerService service; + private List activations; + private EditorActionHandler[] handlers = new EditorActionHandler[] { + new EditorActionHandler(ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS, SourceViewer.CONTENTASSIST_PROPOSALS), + new EditorActionHandler(IWorkbenchCommandConstants.EDIT_UNDO, SourceViewer.UNDO), + new EditorActionHandler(IWorkbenchCommandConstants.EDIT_REDO, SourceViewer.REDO), + }; + + private ISelectionChangedListener selectionListener = new ISelectionChangedListener() { + @Override + public void selectionChanged(final SelectionChangedEvent e) { + IResource resource = (IResource) getStructuredSelection(workspaceViewer).getFirstElement(); + model.setSelectedManifest(resource); + } + }; + + final private DeploymentPropertiesDialogModel model; + private Button buttonEnableJmx; + + public DeploymentPropertiesDialog(Shell parentShell, DeploymentPropertiesDialogModel model) { + super(parentShell); + this.model = model; + this.service = (IHandlerService) PlatformUI.getWorkbench().getAdapter(IHandlerService.class); + this.activations = new ArrayList<>(handlers.length); + } + + @Override + protected Control createDialogArea(Composite parent) { + setTitle("Select Deployment Manifest for project '" + model.getProjectName() + "'"); + Composite container = (Composite) super.createDialogArea(parent); + final Composite composite = new Composite(container, parent.getStyle()); + composite.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create()); + composite.setLayout(new GridLayout()); + + createModeSwitchGroup(composite); + + createFileGroup(composite); + + createResizeSash(composite); + + createYamlContentsGroup(composite); + + if (model.supportsSsh) { + createJmxSshOptionsGroup(composite); + } + + activateHandlers(); + + model.type.addListener(new ValueListener() { + @Override + public void gotValue(LiveExpression exp, ManifestType type) { + GridData gridData; + boolean isFile = type == ManifestType.FILE; + buttonFileManifest.setSelection(isFile); + buttonManualManifest.setSelection(!isFile); + + refreshButton.setEnabled(isFile && !workspaceViewer.getSelection().isEmpty()); + workspaceViewer.getControl().setEnabled(isFile); + fileLabel.setEnabled(isFile); + + gridData = GridDataFactory.copyData((GridData) fileGroup.getLayoutData()); + gridData.exclude = !isFile; + fileGroup.setVisible(isFile); + fileGroup.setLayoutData(gridData); + gridData = GridDataFactory.copyData((GridData) resizeSash.getLayoutData()); + gridData.exclude = !isFile; + resizeSash.setVisible(isFile); + resizeSash.setLayoutData(gridData); + fileGroup.getParent().layout(); + + fileYamlComposite.setVisible(isFile); + gridData = GridDataFactory.copyData((GridData) fileYamlComposite.getLayoutData()); + gridData.exclude = !isFile; + fileYamlComposite.setLayoutData(gridData); + manualYamlComposite.setVisible(!isFile); + gridData = GridDataFactory.copyData((GridData) manualYamlComposite.getLayoutData()); + gridData.exclude = isFile; + manualYamlComposite.setLayoutData(gridData); + yamlGroup.layout(); + yamlGroup.getParent().layout(); + } + }); + + model.getValidator().addListener(new UIValueListener() { + @Override + protected void uiGotValue(LiveExpression exp, ValidationResult value) { + ValidationResult result = exp.getValue(); + if (getButton(IDialogConstants.OK_ID) != null) { + getButton(IDialogConstants.OK_ID).setEnabled(result.status != IStatus.ERROR); + } + setMessage(result.msg, result.getMessageProviderStatus()); + } + }); + + parent.pack(true); + + /* + * Reveal the selected manifest file in the workspace viewer now when + * controls are created and laid out + */ + if (!workspaceViewer.getSelection().isEmpty()) { + workspaceViewer.setSelection(workspaceViewer.getSelection(), true); + } + return container; + } + + private void createJmxSshOptionsGroup(Composite composite) { + Group group = new Group(composite, SWT.NONE); + group.setText("JMX Ssh Tunnel"); + group.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + group.setLayout(new GridLayout(2, true)); + + buttonEnableJmx = new Button(group, SWT.CHECK); + buttonEnableJmx.setText("Enable"); + buttonEnableJmx.setToolTipText("Activate JMX on the deployed app and create an SSH tunnel to it so it can be accessed locally."); + + SwtConnect.checkbox(buttonEnableJmx, model.enableJmxSshTunnel); + } + + private void createModeSwitchGroup(Composite composite) { + Group typeGroup = new Group(composite, SWT.NONE); + typeGroup.setText("Manifest Type"); + typeGroup.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + typeGroup.setLayout(new GridLayout(2, true)); + + buttonFileManifest = new Button(typeGroup, SWT.RADIO); + buttonFileManifest.setText("File"); + buttonFileManifest.setSelection(model.isFileManifestType()); + buttonFileManifest.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (buttonFileManifest.getSelection()) { + model.setManifestType(ManifestType.FILE); + } + } + }); + buttonFileManifest.setLayoutData(GridDataFactory.fillDefaults().create()); + + buttonManualManifest = new Button(typeGroup, SWT.RADIO); + buttonManualManifest.setText("Manual"); + buttonManualManifest.setSelection(model.isManualManifestType()); + buttonManualManifest.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (buttonManualManifest.getSelection()) { + model.setManifestType(ManifestType.MANUAL); + } + } + }); + buttonManualManifest.setLayoutData(GridDataFactory.fillDefaults().create()); + } + + private void createFileGroup(Composite composite) { + fileGroup = new Group(composite, SWT.NONE); + fileGroup.setText("Workspace File"); + fileGroup.setLayout(new GridLayout(2, false)); + int height = DEFAULT_WORKSPACE_GROUP_HEIGHT; + try { + height = getDialogBoundsSettings().getInt(DIALOG_LIST_HEIGHT_SETTING); + } catch (NumberFormatException e) { + // ignore exception + } + fileGroup.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).hint(SWT.DEFAULT, height).create()); + + workspaceViewer = new TreeViewer(fileGroup); + workspaceViewer.setContentProvider(new BaseWorkbenchContentProvider() { + + @Override + public Object[] getChildren(Object element) { + Object[] children = super.getChildren(element); + if (element instanceof IFolder) { + IFolder folder = (IFolder) element; + if (folder.getParent() instanceof IProject) { + if (".settings".equals(folder.getName())) { + List filtered = new ArrayList<>(); + for (Object child : children) { + if (child instanceof IFile) { + // Skip the temporary manifest file for manual mode + IFile f = (IFile) child; + if (DeploymentPropertiesDialogModel.DUMMY_MANUAL_MANIFEST_YML.equals(f.getName()) + || DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML.equals(f.getName())) { + continue; + } + } + filtered.add(child); + } + children = filtered.toArray(new Object[filtered.size()]); + } + } + } + if (element instanceof IProject) { + IJavaProject javaProject = JavaCore.create((IProject)element); + if (javaProject != null) { + List filtered = new ArrayList<>(); + for (Object child : children) { + // Filter out obvious output folders + if (child instanceof IFolder) { + IFolder folder = (IFolder) child; + if ("target".equals(folder.getName()) || "bin".equals(folder.getName())) { + continue; + } + } + filtered.add(child); + } + children = filtered.toArray(new Object[filtered.size()]); + } + } + return children; + } + + }); + workspaceViewer.setLabelProvider(new WorkbenchLabelProvider()); + workspaceViewer.setInput(ResourcesPlugin.getWorkspace().getRoot()); + workspaceViewer.getControl().setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create()); + + /* + * Do not set the selection based on manifest file changes outside of UI. Bad. SWT doesn't like it. + */ + if (model.getSelectedManifest() != null) { + workspaceViewer.setSelection(new StructuredSelection(new Object[] { model.getSelectedManifest() }), true); + } + + workspaceViewer.addSelectionChangedListener(selectionListener); + + Composite fileButtonsComposite = new Composite(fileGroup, SWT.NONE); + GridLayout layout = new GridLayout(); + layout.marginHeight = 0; + layout.marginWidth = 0; + layout.horizontalSpacing = 0; + layout.verticalSpacing = 0; + fileButtonsComposite.setLayout(layout); + fileButtonsComposite.setLayoutData(GridDataFactory.fillDefaults().create()); + + refreshButton = new Button(fileButtonsComposite, SWT.PUSH); + refreshButton.setImage(BootDashActivator.getDefault().getImageRegistry().get(BootDashActivator.REFRESH_ICON)); + refreshButton.setText("Refresh"); + refreshButton.setEnabled(false); + refreshButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + refreshManifests(); + } + }); + refreshButton.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + + fileFilterCombo = new Combo(fileGroup, SWT.DROP_DOWN | SWT.BORDER | SWT.READ_ONLY); + fileFilterCombo.setItems(FILE_FILTER_NAMES); + int selectionIndex = 0; + + IResource manifestFile = model.getSelectedManifest(); + if (manifestFile != null) { + selectionIndex = RESOURCE_FILTERS.length - 1; + for (int i = 0; i < RESOURCE_FILTERS.length; i++) { + boolean accept = true; + for (ViewerFilter filter : RESOURCE_FILTERS[i]) { + accept = filter.select(null, null, manifestFile); + if (!accept) { + break; + } + } + if (accept) { + selectionIndex = i; + break; + } + } + } + workspaceViewer.setFilters(RESOURCE_FILTERS[selectionIndex]); + fileFilterCombo.select(selectionIndex); + fileFilterCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // Remove selection listener to set selection from current pathModel value + workspaceViewer.removeSelectionChangedListener(selectionListener); + workspaceViewer.setFilters(RESOURCE_FILTERS[fileFilterCombo.getSelectionIndex()]); + // Add the selection listener back after the initial value has been set + workspaceViewer.addSelectionChangedListener(selectionListener); + } + }); + } + + private void createResizeSash(Composite composite) { + resizeSash = new Sash(composite, SWT.HORIZONTAL); + resizeSash.setLayoutData(GridDataFactory.fillDefaults().hint(SWT.DEFAULT, 4).grab(true, false).create()); + resizeSash.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + GridData listLayoutData = (GridData) fileGroup.getLayoutData(); + int newHeight = listLayoutData.heightHint + e.y - resizeSash.getBounds().y; + if (newHeight < listLayoutData.minimumHeight) { + newHeight = listLayoutData.minimumHeight; + e.doit = false; + } + listLayoutData.heightHint = newHeight; + fileGroup.setLayoutData(listLayoutData); + fileGroup.getParent().layout(); + } + }); + } + + private void createYamlContentsGroup(Composite composite) { + yamlGroup = new Group(composite, SWT.NONE); + yamlGroup.setText("YAML Content"); + yamlGroup.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create()); + yamlGroup.setLayout(new GridLayout()); + + fileYamlComposite = new Composite(yamlGroup, SWT.NONE); + fileYamlComposite.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create()); + GridLayout layout = new GridLayout(3, false); + layout.marginWidth = 0; + fileYamlComposite.setLayout(layout); + + fileLabel = new Label(fileYamlComposite, SWT.WRAP); + fileLabel.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).align(SWT.FILL, SWT.CENTER).span(3, SWT.DEFAULT).create()); + + model.getFileLabel().addListener(new UIValueListener() { + @Override + protected void uiGotValue(LiveExpression exp, String value) { + if (!fileLabel.isDisposed()) { + fileLabel.setText(exp.getValue()); + } + } + }); + + model.getFileYamlEditor().setContext(CONTEXT_DEPLOYMENT_PROPERTIES_DIALOG); + try { + model.getFileYamlEditor().createControl(fileYamlComposite); + } catch (CoreException e) { + Log.log(e); + } + model.getFileYamlEditor().getViewer().getControl().setLayoutData(GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, 200).create()); + model.fileYamlEditorControlCreated(); + + + manualYamlComposite = new Composite(yamlGroup, SWT.NONE); + manualYamlComposite.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create()); + layout = new GridLayout(); + layout.marginWidth = 0; + manualYamlComposite.setLayout(layout); + + Label manualYamlDescriptionLabel = new Label(manualYamlComposite, SWT.WRAP); + manualYamlDescriptionLabel.setText(model.isManualManifestReadOnly() ? "Preview of the contents of the auto-generated deployment manifest:" : "Edit deployment manifest contents:"); + manualYamlDescriptionLabel.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + + + model.getManualYamlEditor().setContext(CONTEXT_DEPLOYMENT_PROPERTIES_DIALOG); + try { + model.getManualYamlEditor().createControl(manualYamlComposite); + } catch (CoreException e) { + Log.log(e); + } + model.getManualYamlEditor().getViewer().getControl().setLayoutData(GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, 200).create()); + ProjectionViewer manualYamlViewer = model.getManualYamlEditor().getViewer(); + if (model.isManualManifestReadOnly()) { + manualYamlViewer.setEditable(false); + manualYamlViewer.getTextWidget().setCaret(null); + manualYamlViewer.getTextWidget().setCursor(getShell().getDisplay().getSystemCursor(SWT.CURSOR_ARROW)); + } + model.manualYamlEditorControlCreated(); + + /* + * Set the proper Font on the YAML viewers + */ + model.getFileYamlEditor().getViewer().getTextWidget().setFont(JFaceResources.getTextFont()); + manualYamlViewer.getTextWidget().setFont(JFaceResources.getTextFont()); + } + + private void activateHandlers() { + if (service != null) { + for (EditorActionHandler handler : handlers) { + activations.add(service.activateHandler(handler.getActionId(), handler)); + } + } + } + + private void deactivateHandlers() { + if (service != null) { + for (IHandlerActivation activation : activations) { + service.deactivateHandler(activation); + } + activations.clear(); + } + } + + private void refreshManifests() { + IResource selectedResource = (IResource) ((IStructuredSelection) workspaceViewer.getSelection()).getFirstElement(); + final IResource resourceToRefresh = selectedResource instanceof IFile ? selectedResource.getParent() : selectedResource; + Job job = new Job("Refreshing resources for '" + resourceToRefresh.getName() + "'") { + @Override + protected IStatus run(IProgressMonitor monitor) { + IStatus status = Status.OK_STATUS; + try { + resourceToRefresh.refreshLocal(IResource.DEPTH_INFINITE, monitor); + } catch (CoreException e) { + status = e.getStatus(); + } + getParentShell().getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + // Remove selection listener to set selection from current pathModel value + workspaceViewer.removeSelectionChangedListener(selectionListener); + workspaceViewer.refresh(resourceToRefresh); + // Add the selection listener back after the initial value has been set + workspaceViewer.addSelectionChangedListener(selectionListener); + } + }); + return status; + } + }; + job.setRule(resourceToRefresh); + job.schedule(); + } + + @Override + protected boolean isResizable() { + return true; + } + + @Override + protected IDialogSettings getDialogBoundsSettings() { + return DialogSettings.getOrCreateSection(BootDashActivator.getDefault().getDialogSettings(), "ManifestFileDialog"); + } + + @Override + public boolean close() { + if (getReturnCode() == IDialogConstants.CANCEL_ID) { + model.cancelPressed(); + } else { + if (!model.okPressed()) { + return false; + } + } + getDialogBoundsSettings().put(DIALOG_LIST_HEIGHT_SETTING, ((GridData)fileGroup.getLayoutData()).heightHint); + boolean close = super.close(); + dispose(); + return close; + } + + protected void dispose() { + /* + * Deactivate handlers for key bindings + */ + deactivateHandlers(); + + model.dispose(); + } + + // TODO: this should be replaced with TreeViewer.getStructuredSelection once we drop support for Eclipse 4.4 + // the TreeViewer.getStructuredSelection() API got introduced in Eclipse 4.5 + private ITreeSelection getStructuredSelection(TreeViewer treeViewer) { + ISelection selection = treeViewer.getSelection(); + if (selection instanceof ITreeSelection) { + return (ITreeSelection) selection; + } + throw new ClassCastException("AbstractTreeViewer should return an instance of ITreeSelection from its getSelection() method."); //$NON-NLS-1$ + } + + @Override + protected int getDialogBoundsStrategy() { + return DIALOG_PERSISTSIZE; + } + + @Override + protected Point getInitialSize() { + Point size = super.getInitialSize(); + /* + * If manual mode is selected fileGroup is missing and not accounted in + * the size of the dialog shell. Add its height here manually if dialog + * size was not persisted previously + */ + GridData fileGroupLayoutData = (GridData)fileGroup.getLayoutData(); + if (fileGroupLayoutData.exclude) { + try { + /* + * Hack: check if dialog width/height was persisted. If + * persisted then no need to calculate dialog size + */ + getDialogBoundsSettings().getInt("DIALOG_WIDTH"); + } catch (NumberFormatException e) { + /* + * Exception is thrown if dialog width/height cannot be read + * from storage + */ + size.y += fileGroupLayoutData.heightHint; + } + } + return size; + } + + private class EditorActionHandler extends AbstractHandler { + + private String actionId; + private int operationId; + + public EditorActionHandler(String actionId, int operationId) { + super(); + this.actionId = actionId; + this.operationId = operationId; + } + + public String getActionId() { + return actionId; + } + + @Override + public Object execute(ExecutionEvent arg0) throws ExecutionException { + ProjectionViewer manualYamlViewer = model.getManualYamlEditor().getViewer(); + ProjectionViewer fileYamlViewer = model.getFileYamlEditor().getViewer(); + if (manualYamlViewer.isEditable() && manualYamlViewer.getControl().isVisible() + && manualYamlViewer.getTextWidget().isFocusControl()) { + manualYamlViewer.doOperation(operationId); + } else if (fileYamlViewer.isEditable() && fileYamlViewer.getControl().isVisible() + && fileYamlViewer.getTextWidget().isFocusControl()) { + fileYamlViewer.doOperation(operationId); + } + return null; + } + } + + public static IFile findManifestYamlFile(IProject project) { + if (project == null) { + return null; + } + IFile file = project.getFile("manifest.yml"); + if (file.exists()) { + return file; + } + IFile yamlFile = null; + try { + for (IResource r : project.members()) { + if (r instanceof IFile) { + file = (IFile) r; + if (MANIFEST_YAML_FILE_FILTER.select(null, project, file)) { + return file; + } else if (YAML_FILE_FILTER.select(null, project, file)) { + yamlFile = file; + } + } + } + } catch (CoreException e) { + // ignore + } + return yamlFile; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/DeploymentPropertiesDialogModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/DeploymentPropertiesDialogModel.java new file mode 100644 index 000000000..1cf4c7e97 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/DeploymentPropertiesDialogModel.java @@ -0,0 +1,971 @@ +/******************************************************************************* + * Copyright (c) 2016, 2020 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.cf.dialogs; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.jdt.internal.ui.JavaPlugin; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.AnnotationModelEvent; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.IAnnotationModelListener; +import org.eclipse.jface.text.source.IAnnotationModelListenerExtension; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IPropertyListener; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.internal.genericeditor.ExtensionBasedTextViewerConfiguration; +import org.eclipse.ui.part.FileEditorInput; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.AppNameAnnotation; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.AppNameAnnotationModel; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.AppNameAnnotationSupport; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.ApplicationManifestHandler; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudData; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.launch.properties.EmbeddedEditor; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.DumperOptions.FlowStyle; +import org.yaml.snakeyaml.Yaml; + +@SuppressWarnings("restriction") +public class DeploymentPropertiesDialogModel extends AbstractDisposable { + + public static final String LSP_ERROR_ANNOTATION_TYPE = "org.eclipse.ui.workbench.texteditor.error"; + + public static final String DUMMY_FILE_MANIFEST_YML = ".file-manifest.yml"; + public static final String DUMMY_MANUAL_MANIFEST_YML = ".manual-manifest.yml"; + public static final String UNKNOWN_DEPLOYMENT_MANIFEST_TYPE_MUST_BE_EITHER_FILE_OR_MANUAL = "Unknown deployment manifest type. Must be either 'File' or 'Manual'."; + public static final String NO_SUPPORT_TO_DETERMINE_APP_NAMES = "Support for determining application names is unavailable"; + public static final String MANIFEST_DOES_NOT_CONTAIN_DEPLOYMENT_PROPERTIES_FOR_APPLICATION_WITH_NAME = "Manifest does not contain deployment properties for application with name ''{0}''."; + public static final String APPLICATION_NAME_NOT_SELECTED = "Application name not selected"; + public static final String MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED = "Manifest does not have any application defined."; + public static final String ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY = "Enter deployment manifest YAML manually."; + public static final String CURRENT_GENERATED_DEPLOYMENT_MANIFEST = "Current generated deployment manifest."; + public static final String CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM = "Choose an existing deployment manifest YAML file from the local file system."; + public static final String DEPLOYMENT_MANIFEST_FILE_NOT_SELECTED = "Deployment manifest file not selected."; + public static final String MANIFEST_YAML_ERRORS = "Deployment manifest YAML has errors."; + + public static enum ManifestType { + FILE, + MANUAL + } + + private UserInteractions ui; + + private abstract class AbstractSubModel extends AbstractDisposable { + + EmbeddedEditor editor; + + AppNameAnnotationSupport appNameAnnotationSupport; + + final LiveVariable editorControlCreated; + + final LiveVariable selectedFile; + + final LiveExpression editorInput; + + LiveExpression appNameAnnotationModel; + + LiveExpression resourceAnnotationModel; + + LiveExpression> applicationNames; + + LiveExpression errorsInYaml; + + LiveExpression selectedAppName; + + /** + * When dialog closed and viewer disposed live expression would still keep the document which will be used to detrmine DeploymentProperties + */ + final LiveExpression document; + + protected IFile getFile() { + IResource r = selectedFile.getValue(); + return r instanceof IFile ? (IFile) r : null; + } + + protected void saveOrDiscardIfNeeded() { + FileEditorInput input = editorInput.getValue(); + if (input != null) { + saveOrDiscardIfNeeded(input); + } + } + + protected void saveOrDiscardIfNeeded(FileEditorInput file) { + if (editor.isDirty()) { + if (ui.confirmOperation("Changes Detected", "Manifest file '" + file.getFile().getFullPath().toOSString() + + "' has been changed. Do you want to save changes or discard them?", new String[] {"Save", "Don't Save"}, 0) == 0) { + editor.doSave(new NullProgressMonitor()); + } + } + } + + abstract String getManifestContents(); + + /** + * Return manifest from which contents are takes as an {@link IFile} + * Return null if manifest content doesn't come from a file + * @return + */ + abstract IFile getManifest(); + + CloudApplicationDeploymentProperties getDeploymentProperties() throws Exception { + CloudApplicationDeploymentProperties deploymentProperties = toDeploymentProperties(cloudData, + getManifestContents(), project, + deployedApp == null ? selectedAppName.getValue() : getDeployedAppName()); + if (deploymentProperties != null) { + deploymentProperties.setManifestFile(getManifest()); + } + return deploymentProperties; + } + + protected AbstractSubModel(String fixedAppName) { + IPreferenceStore preferenceStore = JavaPlugin.getDefault().getCombinedPreferenceStore(); + this.editor = new EmbeddedEditor(editor -> new ExtensionBasedTextViewerConfiguration(editor, preferenceStore), preferenceStore, false, true); + + editorControlCreated = new LiveVariable<>(false); + + selectedFile = new LiveVariable<>(); + + editorControlCreated.addListener((exp, value) -> { + if (appNameAnnotationSupport == null && value != null && value.booleanValue()) { + appNameAnnotationSupport = new AppNameAnnotationSupport(editor.getViewer(), + editor.getAnnotationAccess(), editor.getSharedColors(), fixedAppName); + } + }); + + editorInput = new LiveExpression() { + + { + dependsOn(selectedFile); + } + + @Override + protected FileEditorInput compute() { + if (editor != null) { + IFile file = getFile(); + FileEditorInput currentInput = getValue(); + boolean changed = currentInput == null || !currentInput.getFile().equals(file); + if (changed) { + if (currentInput != null) { + saveOrDiscardIfNeeded(currentInput); + } + try { + if (file != null) { + FileEditorInput input = new FileEditorInput(file); + editor.setInput(input); + return input; + } else { + editor.setInput(null); + return null; + } + } catch (CoreException e) { + Log.log(e); + } + } + } + return null; + } + + }; + + appNameAnnotationModel = new LiveExpression() { + { + dependsOn(editorControlCreated); + dependsOn(editorInput); + } + + @Override + protected AppNameAnnotationModel compute() { + if (editor != null && editor.getViewer() != null) { + return AppNameAnnotationSupport.getAppNameAnnotationModel(editor.getViewer()); + } + return null; + } + + }; + + resourceAnnotationModel = new LiveExpression() { + + { + dependsOn(editorControlCreated); + dependsOn(editorInput); + } + + @Override + protected IAnnotationModel compute() { + if (editor != null && editor.getViewer() != null) { + return editor.getViewer().getAnnotationModel(); + } + return null; + } + + }; + + applicationNames = new LiveExpression>() { + + private AppNameAnnotationModel attachedTo = null; + private AnnotationModelListener listener = new AnnotationModelListener() { + @Override + public void modelChanged(AnnotationModelEvent event) { + refresh(); + } + }; + + { + dependsOn(appNameAnnotationModel); + } + + @Override + protected List compute() { + AppNameAnnotationModel annotationModel = appNameAnnotationModel.getValue(); + if (annotationModel != null) { + attachListener(annotationModel); + List applicationNames = new ArrayList<>(); + for (Iterator itr = annotationModel.getAnnotationIterator(); itr.hasNext();) { + Annotation next = itr.next(); + if (next instanceof AppNameAnnotation) { + AppNameAnnotation a = (AppNameAnnotation) next; + applicationNames.add(a.getText()); + } + } + return applicationNames; + } + return Collections.emptyList(); + } + + synchronized private void attachListener(AppNameAnnotationModel annotationModel) { + if (attachedTo == annotationModel) { + return; + } + if (attachedTo != null) { + attachedTo.removeAnnotationModelListener(listener); + } + if (annotationModel != null) { + annotationModel.addAnnotationModelListener(listener); + } + attachedTo = annotationModel; + } + + }; + + errorsInYaml = new LiveExpression() { + + private IAnnotationModel attachedTo = null; + private AnnotationModelListener listener = new AnnotationModelListener() { + @Override + public void modelChanged(AnnotationModelEvent event) { + refresh(); + } + }; + + { + dependsOn(resourceAnnotationModel); + } + + { + onDispose((d) -> { + if (attachedTo != null) { + attachedTo.removeAnnotationModelListener(listener); + } + }); + } + + @Override + protected Boolean compute() { + IAnnotationModel annotationModel = resourceAnnotationModel.getValue(); + if (annotationModel != null) { + attachListener(annotationModel); + for (Iterator itr = annotationModel.getAnnotationIterator(); itr.hasNext();) { + Annotation next = itr.next(); + if (LSP_ERROR_ANNOTATION_TYPE.equals(next.getType())) { + return Boolean.TRUE; + } + } + } + return Boolean.FALSE; + } + + synchronized private void attachListener(IAnnotationModel annotationModel) { + if (attachedTo == annotationModel) { + return; + } + if (attachedTo != null) { + attachedTo.removeAnnotationModelListener(listener); + } + if (annotationModel != null) { + annotationModel.addAnnotationModelListener(listener); + } + attachedTo = annotationModel; + } + }; + + selectedAppName = new LiveExpression() { + + private AppNameAnnotationModel attachedTo = null; + private AnnotationModelListener listener = new AnnotationModelListener() { + @Override + public void modelChanged(AnnotationModelEvent event) { + refresh(); + } + }; + + { + dependsOn(appNameAnnotationModel); + } + + { + onDispose((d) -> { + if (attachedTo != null) { + attachedTo.removeAnnotationModelListener(listener); + } + }); + } + + @Override + protected String compute() { + AppNameAnnotationModel annotationModel = appNameAnnotationModel.getValue(); + if (annotationModel != null) { + attachListener(annotationModel); + AppNameAnnotation a = annotationModel.getSelectedAppAnnotation(); + if (a != null) { + return a.getText(); + } + } + return null; + } + + synchronized private void attachListener(AppNameAnnotationModel annotationModel) { + if (attachedTo == annotationModel) { + return; + } + if (attachedTo != null) { + attachedTo.removeAnnotationModelListener(listener); + } + if (annotationModel != null) { + annotationModel.addAnnotationModelListener(listener); + } + attachedTo = annotationModel; + } + + }; + + document = new LiveExpression(new Document("")) { + { + dependsOn(editorInput); + dependsOn(editorControlCreated); + } + + @Override + protected IDocument compute() { + IDocument doc = editor == null || editor.getViewer() == null ? null : editor.getViewer().getDocument(); + return doc; + } + }; + + onDispose((d) -> { + appNameAnnotationSupport.dispose(); + editorControlCreated.dispose(); + editorInput.dispose(); + applicationNames.dispose(); + appNameAnnotationModel.dispose(); + errorsInYaml.dispose(); + resourceAnnotationModel.dispose(); + selectedAppName.dispose(); + document.dispose(); + editor.dispose(); + }); + } + + } + + public class FileDeploymentPropertiesDialogModel extends AbstractSubModel { + + final private LiveExpression fileLabel = new LiveExpression() { + { + dependsOn(editorInput); + } + + @Override + protected String compute() { + FileEditorInput input = editorInput.getValue(); + if (input != null) { + return editorInput.getValue().getFile().getFullPath().toOSString() + (editor.isDirty() ? "*" : ""); + } + return ""; + } + + }; + + private final IPropertyListener editorListener = (Object source, int propId) -> { + if (propId == IEditorPart.PROP_DIRTY) { + fileLabel.refresh(); + } + }; + + Validator validator = new Validator() { + + { + dependsOn(editorInput); + dependsOn(appNameAnnotationModel); + dependsOn(errorsInYaml); + dependsOn(applicationNames); + dependsOn(selectedAppName); + } + + @Override + protected ValidationResult compute() { + ValidationResult result = ValidationResult.OK; + + if (editorInput.getValue() == null) { + result = ValidationResult.error(DEPLOYMENT_MANIFEST_FILE_NOT_SELECTED); + } + + if (result.isOk()) { + AppNameAnnotationModel appNamesModel = appNameAnnotationModel.getValue(); + if (appNamesModel == null) { + result = ValidationResult.error(NO_SUPPORT_TO_DETERMINE_APP_NAMES); + } + if (result.isOk()) { + String appName = getDeployedAppName(); + if (applicationNames.getValue().isEmpty()) { + result = ValidationResult.error(MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED); + } else { + if (errorsInYaml.getValue().booleanValue()) { + result = ValidationResult.error(MANIFEST_YAML_ERRORS); + } else { + String selectedAnnotation = selectedAppName.getValue(); + if (appName == null) { + if (selectedAnnotation == null) { + result = ValidationResult.error(APPLICATION_NAME_NOT_SELECTED); + } + } else { + if (selectedAnnotation == null || !appName.equals(selectedAnnotation)) { + result = ValidationResult.error(MessageFormat.format( + MANIFEST_DOES_NOT_CONTAIN_DEPLOYMENT_PROPERTIES_FOR_APPLICATION_WITH_NAME, + appName)); + } + } + } + } + } + } + + if (result.isOk()) { + result = ValidationResult.info(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM); + } + + return result; + } + + }; + + private IFile tempFile; + + { + onDispose((d) -> { + if (tempFile != null && tempFile.exists()) { + try { + tempFile.delete(true, new NullProgressMonitor()); + } catch (CoreException e) { + Log.log(e); + } + } + editor.removePropertyListener(editorListener); + validator.dispose(); + fileLabel.dispose(); + }); + } + + FileDeploymentPropertiesDialogModel(String fixedAppName) { + super(fixedAppName); + editor.addPropertyListener(editorListener); + } + + public void init(IFile tempFile) { + this.tempFile = tempFile; + if (getManifest() == null) { + // No manifest file? Generate dumb empty manifest YAML to get proper Manifest LS + // based reconciler, CA etc + try { + generateTempManifestFile(tempFile, ""); + editor.init(null, new FileEditorInput(tempFile)); + } catch (CoreException e) { + Log.log(e); + } + } + } + + public IAnnotationModel getAnnotationModel() { + return editor.getViewer().getAnnotationModel(); + } + + @Override + String getManifestContents() { + return document.getValue().get(); + } + + @Override + IFile getManifest() { + return getFile(); + } + + void reopenSameFile() { + document.refresh(); + } + + } + + public class ManualDeploymentPropertiesDialogModel extends AbstractSubModel { + + private IFile tempFile; + + private boolean readOnly; + + Validator validator = new Validator() { + + { + dependsOn(appNameAnnotationModel); + dependsOn(errorsInYaml); + dependsOn(applicationNames); + dependsOn(selectedAppName); + } + + @Override + protected ValidationResult compute() { + ValidationResult result = ValidationResult.OK; + + AppNameAnnotationModel appNamesModel = appNameAnnotationModel.getValue(); + if (appNamesModel == null) { + result = ValidationResult.error(NO_SUPPORT_TO_DETERMINE_APP_NAMES); + } + if (result.isOk()) { + String appName = getDeployedAppName(); + if (applicationNames.getValue().isEmpty()) { + result = ValidationResult.error(MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED); + } else { + if (errorsInYaml.getValue().booleanValue()) { + result = ValidationResult.error(MANIFEST_YAML_ERRORS); + } else { + String selectedAnnotation = selectedAppName.getValue(); + if (appName == null) { + if (selectedAnnotation == null) { + result = ValidationResult.error(APPLICATION_NAME_NOT_SELECTED); + } + } else { + if (selectedAnnotation == null || !appName.equals(selectedAnnotation)) { + result = ValidationResult.error(MessageFormat.format( + MANIFEST_DOES_NOT_CONTAIN_DEPLOYMENT_PROPERTIES_FOR_APPLICATION_WITH_NAME, + appName)); + } + } + } + } + } + + if (result.isOk()) { + result = ValidationResult.info(readOnly ? CURRENT_GENERATED_DEPLOYMENT_MANIFEST : ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY); + } + + return result; + } + }; + + { + onDispose((d) -> { + validator.dispose(); + try { + if (tempFile != null) { + if (tempFile.exists()) { + tempFile.delete(true, new NullProgressMonitor()); + } + tempFile = null; + } + } catch (CoreException e) { + e.printStackTrace(); + } + }); + } + + + ManualDeploymentPropertiesDialogModel(String fixedAppName, boolean readOnly) { + super(fixedAppName); + this.readOnly = readOnly; + } + + public void init(IFile tempFile) { + this.tempFile = tempFile; + try { + generateTempManifestFile(tempFile, generateDefaultContent()); + } catch (CoreException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + try { + editor.init(null, new FileEditorInput(tempFile)); + selectedFile.setValue(tempFile); + } catch (PartInitException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public void setText(String s) { + if (readOnly) { + throw new IllegalStateException("The model is read-only!"); + } + IDocument doc = document.getValue(); + if (doc != null) { + doc.set(s); + } + } + + public String getText() { + IDocument doc = document.getValue(); + return doc == null ? null : doc.get(); + } + + public IAnnotationModel getAnnotationModel() { + return editor.getViewer().getAnnotationModel(); + } + + @Override + String getManifestContents() { + return getText(); + } + + @Override + IFile getManifest() { + return null; + } + + } + + private abstract class AnnotationModelListener implements IAnnotationModelListener, IAnnotationModelListenerExtension { + + @Override + public void modelChanged(IAnnotationModel model) { + // Leave empty. AnnotationModelEvent method is the one that will be called + } + + @Override + abstract public void modelChanged(AnnotationModelEvent event); + + } + + + final public LiveVariable type = new LiveVariable<>(); + final public LiveVariable enableJmxSshTunnel = new LiveVariable<>(); + + final private CFApplication deployedApp; + + final private CloudData cloudData; + + final IProject project; + + final private FileDeploymentPropertiesDialogModel fileModel; + + final private ManualDeploymentPropertiesDialogModel manualModel; + + private boolean isCancelled = false; + + final private Validator validator; + + final public boolean supportsSsh; + + public DeploymentPropertiesDialogModel(UserInteractions ui, CloudData cloudData, IProject project, CFApplication deployedApp, boolean supportsSsh) { + super(); + this.supportsSsh = supportsSsh; + this.ui = ui; + this.deployedApp = deployedApp; + this.cloudData = cloudData; + this.project = project; + this.manualModel = new ManualDeploymentPropertiesDialogModel(getDeployedAppName(), deployedApp != null); + this.fileModel = new FileDeploymentPropertiesDialogModel(getDeployedAppName()); + + this.validator = new Validator() { + + { + dependsOn(type); + dependsOn(fileModel.validator); + dependsOn(manualModel.validator); + } + + @Override + protected ValidationResult compute() { + if (isFileManifestType()) { + return fileModel.validator.getValue(); + } else if (isManualManifestType()) { + return manualModel.validator.getValue(); + } else { + return ValidationResult.error(UNKNOWN_DEPLOYMENT_MANIFEST_TYPE_MUST_BE_EITHER_FILE_OR_MANUAL); + } + } + + }; + + onDispose((d) -> { + fileModel.dispose(); + manualModel.dispose(); + }); + + } + + public void initFileModel() { + fileModel.init(project.getFolder(".settings").getFile(DUMMY_FILE_MANIFEST_YML)); + } + + public void initManualModel() { + manualModel.init(project.getFolder(".settings").getFile(DUMMY_MANUAL_MANIFEST_YML)); + } + + public CloudApplicationDeploymentProperties getDeploymentProperties() throws Exception { + if (isCancelled) { + throw new OperationCanceledException(); + } + if (type.getValue() == null) { + return null; + } + CloudApplicationDeploymentProperties props = null; + switch (type.getValue()) { + case FILE: + props = fileModel.getDeploymentProperties(); + break; + case MANUAL: + props = manualModel.getDeploymentProperties(); + break; + default: + } + if (props!=null) { + Boolean enableJmx = enableJmxSshTunnel.getValue(); + props.setEnableJmxSshTunnel(enableJmx!=null && enableJmx); + } + return props; + } + + public void cancelPressed() { + fileModel.saveOrDiscardIfNeeded(); + isCancelled = true; +// try { +// fileModel.editor.setInput(null); +// manualModel.editor.setInput(null); +// } catch (CoreException e) { +// Log.log(e); +// } + } + + public boolean okPressed() { + fileModel.saveOrDiscardIfNeeded(); + isCancelled = false; + try { + CloudApplicationDeploymentProperties deploymentProperties = getDeploymentProperties(); +// fileModel.editor.setInput(null); +// manualModel.editor.setInput(null); + return deploymentProperties != null; + } catch (Exception e) { + fileModel.reopenSameFile(); + ui.errorPopup("Invalid YAML content", ExceptionUtil.getMessage(e)); + return false; + } + } + + public void setSelectedManifest(IResource manifest) { + fileModel.selectedFile.setValue(manifest); + } + + public void setManualManifest(String manifestText) { + manualModel.setText(manifestText); + } + + public void setManifestType(ManifestType type) { + this.type.setValue(type); + } + + public String getProjectName() { + return project.getName(); + } + + public IProject getProject() { + return project; + } + + public boolean isFileManifestType() { + return type.getValue() == ManifestType.FILE; + } + + public boolean isManualManifestType() { + return type.getValue() == ManifestType.MANUAL; + } + + public IResource getSelectedManifest() { + return fileModel.selectedFile.getValue(); + } + + public String getDeployedAppName() { + return deployedApp == null ? null : deployedApp.getName(); + } + + public EmbeddedEditor getFileYamlEditor() { + return fileModel.editor; + } + + public EmbeddedEditor getManualYamlEditor() { + return manualModel.editor; + } + + public IDocument getManualDocument() { + return manualModel.document.getValue(); + } + + public boolean isManualManifestReadOnly() { + return deployedApp!=null; + } + + public IDocument getFileDocument() { + return fileModel.document.getValue(); + } + + public LiveExpression getFileEditorInput() { + return fileModel.editorInput; + } + + public LiveExpression getFileLabel() { + return fileModel.fileLabel; + } + + private CFApplication getDeployedApp() { + return deployedApp; + } + + public boolean isCanceled() { + return isCancelled; + } + + public Validator getValidator() { + return validator; + } + + public LiveExpression getManualAppNameAnnotationModel() { + return manualModel.appNameAnnotationModel; + } + + public LiveExpression getFileAppNameAnnotationModel() { + return fileModel.appNameAnnotationModel; + } + + public void fileYamlEditorControlCreated() { + fileModel.editorControlCreated.setValue(true); + } + + public void manualYamlEditorControlCreated() { + manualModel.editorControlCreated.setValue(true); + } + + public IAnnotationModel getManualResourceAnnotationModel() { + return manualModel.resourceAnnotationModel.getValue(); + } + + public IAnnotationModel getFileResourceAnnotationModel() { + return fileModel.resourceAnnotationModel.getValue(); + } + + public String getFileSelectedAppName() { + return fileModel.selectedAppName.getValue(); + } + + public String getManualSelectedAppName() { + return manualModel.selectedAppName.getValue(); + } + + private String generateDefaultContent() { + CloudApplicationDeploymentProperties props = CloudApplicationDeploymentProperties.getFor(project, cloudData, + getDeployedApp()); + Map yaml = ApplicationManifestHandler.toYaml(props, cloudData); + DumperOptions options = new DumperOptions(); + options.setExplicitStart(true); + options.setCanonical(false); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(FlowStyle.BLOCK); + return new Yaml(options).dump(yaml); + } + + + private static IFile generateTempManifestFile(IFile manifestFile, String content) throws CoreException { + if (manifestFile.exists()) { + manifestFile.setContents(new ByteArrayInputStream(content.getBytes()), true, false, new NullProgressMonitor()); + } else { + if (!manifestFile.getParent().exists()) { + ((IFolder)manifestFile.getParent()).create(true, true, new NullProgressMonitor()); + } + manifestFile.create(new ByteArrayInputStream(content.getBytes()), true, new NullProgressMonitor()); + } + return manifestFile; + } + + public static CloudApplicationDeploymentProperties toDeploymentProperties(CloudData cloudData, String yaml, IProject project, String applicationName) throws Exception { + List propsList = new ApplicationManifestHandler(project, cloudData, null) { + @Override + protected InputStream getInputStream() throws Exception { + return new ByteArrayInputStream(yaml.getBytes()); + } + }.load(new NullProgressMonitor()); + /* + * If "Select Manifest..." action is invoked appName is not null, + * but we should allow for any manifest file selected for now. Hence + * set the applicationName var to null in that case + */ + CloudApplicationDeploymentProperties deploymentProperties = null; + if (applicationName == null) { + deploymentProperties = propsList.get(0); + } else { + for (CloudApplicationDeploymentProperties p : propsList) { + if (applicationName.equals(p.getAppName())) { + deploymentProperties = p; + break; + } + } + } + return deploymentProperties; + } + + public CloudApplicationDeploymentProperties getDeploymentProperties(String yaml, String appName) throws Exception { + if (yaml == null) { + yaml = generateDefaultContent(); + } + return toDeploymentProperties(cloudData, yaml, project, appName); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/OrgsAndSpaces.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/OrgsAndSpaces.java new file mode 100644 index 000000000..6b0c4df15 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/OrgsAndSpaces.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.dialogs; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.runtime.CoreException; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFOrganization; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFSpace; + +/** + * Hierarchical representation of existing orgs and spaces in a Cloud Foundry + * target. + * + */ +public class OrgsAndSpaces { + + private final List originalSpaces; + private Map> orgIDtoSpaces; + private Map orgIDtoOrg; + + /** + * + * @param spaces + * a flat list of all spaces for a given set of credentials and + * server URL. Should not be empty or null. + * @throws CoreException + * if given cloud server does not support orgs and spaces + */ + public OrgsAndSpaces(List spaces) { + this.originalSpaces = spaces; + setValues(); + } + + public CFSpace getSpace(String orgName, String spaceName) { + List oSpaces = orgIDtoSpaces.get(orgName); + if (oSpaces != null) { + for (CFSpace clSpace : oSpaces) { + if (clSpace.getName().equals(spaceName)) { + return clSpace; + } + } + } + return null; + } + + public List getOrgs() { + + Collection orgList = orgIDtoOrg.values(); + return new ArrayList(orgList); + } + + protected void setValues() { + orgIDtoSpaces = new HashMap>(); + orgIDtoOrg = new HashMap(); + + for (CFSpace clSpace : originalSpaces) { + CFOrganization org = clSpace.getOrganization(); + List spaces = orgIDtoSpaces.get(org.getName()); + if (spaces == null) { + spaces = new ArrayList(); + orgIDtoSpaces.put(org.getName(), spaces); + orgIDtoOrg.put(org.getName(), org); + } + + spaces.add(clSpace); + } + } + + /** + * @param orgName + * @return + */ + public List getOrgSpaces(String orgName) { + return orgIDtoSpaces.get(orgName); + } + + /** + * @return all spaces available for the given account. Never null, although + * may be empty if no spaces are resolved. + */ + public List getAllSpaces() { + return originalSpaces != null ? new ArrayList(originalSpaces) : new ArrayList(0); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/OrgsAndSpacesWizard.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/OrgsAndSpacesWizard.java new file mode 100644 index 000000000..ee55f6171 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/OrgsAndSpacesWizard.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.dialogs; + +import org.eclipse.jface.wizard.Wizard; + +/** + * Creates a new server instance from a selected space in an organization and + * spaces viewer in the wizard. It only creates the server instance if another + * server instance to that space does not exist. + */ +public class OrgsAndSpacesWizard extends Wizard { + + private OrgsAndSpacesWizardPage cloudSpacePage; + + private CloudFoundryTargetWizardModel properties; + + public OrgsAndSpacesWizard( + CloudFoundryTargetWizardModel targetProperties) { + cloudSpacePage = new OrgsAndSpacesWizardPage(targetProperties); + this.properties = targetProperties; + setWindowTitle("Select space for: " + targetProperties.getUrl()); + setNeedsProgressMonitor(true); + } + + @Override + public void addPages() { + cloudSpacePage.setWizard(this); + addPage(cloudSpacePage); + } + + @Override + public boolean performFinish() { + return properties.getSpaceName() != null && properties.getOrganizationName() != null; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/OrgsAndSpacesWizardPage.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/OrgsAndSpacesWizardPage.java new file mode 100644 index 000000000..ea868908b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/OrgsAndSpacesWizardPage.java @@ -0,0 +1,273 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.dialogs; + +import java.util.List; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerSorter; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFEntity; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFOrganization; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFSpace; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +/** + * Wizard page to allow users to select a target cloud space when cloning an + * existing server. + */ +class OrgsAndSpacesWizardPage extends WizardPage implements ValueListener { + + protected TreeViewer orgsSpacesViewer; + + private final CloudFoundryTargetWizardModel targetProperties; + + private final OrgsAndSpaces spaces; + + private boolean canFinish = false; + + OrgsAndSpacesWizardPage(CloudFoundryTargetWizardModel targetWizardModel) { + super("Select an Org and Space"); + setTitle("Select an Org and Space"); + setDescription("Select a space in " + targetWizardModel.getUrl()); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/wizban_cloudfoundry.png")); + this.targetProperties = targetWizardModel; + this.spaces = targetWizardModel.getSpaces(); + targetWizardModel.getValidator().addListener(this); + } + + @Override + public void dispose() { + this.targetProperties.getValidator().removeListener(this); + super.dispose(); + } + + @Override + public boolean isPageComplete() { + return canFinish && targetProperties.getSpaceName() != null && targetProperties.getOrganizationName() != null; + } + + @Override + public void createControl(Composite parent) { + Composite mainComposite = new Composite(parent, SWT.NONE); + GridLayoutFactory.fillDefaults().numColumns(1).margins(5, 5).applyTo(mainComposite); + GridDataFactory.fillDefaults().grab(true, true).applyTo(mainComposite); + + Label orgLabel = new Label(mainComposite, SWT.NONE); + GridDataFactory.fillDefaults().grab(false, false).applyTo(orgLabel); + orgLabel.setText("Orgs:"); + + Tree orgTable = new Tree(mainComposite, SWT.BORDER | SWT.SINGLE); + + GridDataFactory.fillDefaults().grab(true, true).applyTo(orgTable); + + orgsSpacesViewer = new TreeViewer(orgTable); + + orgsSpacesViewer.setContentProvider(new TableContentProvider()); + orgsSpacesViewer.setLabelProvider(new SpacesLabelProvider()); + orgsSpacesViewer.setSorter(new SpacesSorter()); + + orgsSpacesViewer.addSelectionChangedListener(new ISelectionChangedListener() { + + public void selectionChanged(SelectionChangedEvent event) { + refresh(); + } + }); + + setControl(mainComposite); + + setInput(); + } + + public void setInput() { + List orgInput = spaces.getOrgs(); + + CFOrganization[] organizationInput = orgInput.toArray(new CFOrganization[orgInput.size()]); + orgsSpacesViewer.setInput(organizationInput); + + // Expand all first, so that child elements can be selected + orgsSpacesViewer.setExpandedElements(organizationInput); + + setInitialSelectionInViewer(); + } + + protected void setInitialSelectionInViewer() { + + CFSpace selectedSpace = spaces.getAllSpaces().get(0); + + if (selectedSpace != null) { + setSpaceInProperties(selectedSpace); + setSelectionInViewer(selectedSpace); + } + } + + protected void setSelectionInViewer(CFSpace selectedSpace) { + // Now set the cloud space in the tree + Tree tree = orgsSpacesViewer.getTree(); + TreeItem[] orgItems = tree.getItems(); + if (orgItems != null) { + TreeItem orgItem = null; + + // Find the tree item corresponding to the cloud space's + // org + for (TreeItem item : orgItems) { + Object treeObj = item.getData(); + if (treeObj instanceof CFOrganization + && ((CFOrganization) treeObj).getName().equals(selectedSpace.getOrganization().getName())) { + orgItem = item; + break; + + } + } + + if (orgItem != null) { + TreeItem[] children = orgItem.getItems(); + if (children != null) { + for (TreeItem childItem : children) { + Object treeObj = childItem.getData(); + if (treeObj instanceof CFSpace + && ((CFSpace) treeObj).getName().equals(selectedSpace.getName())) { + tree.select(childItem); + break; + } + } + } + } + } + } + + protected void refresh() { + if (orgsSpacesViewer != null) { + + Tree tree = orgsSpacesViewer.getTree(); + TreeItem[] selectedItems = tree.getSelection(); + if (selectedItems != null && selectedItems.length > 0) { + // It's a single selection tree, so only get the first selection + Object selectedObj = selectedItems[0].getData(); + setSpaceInProperties(selectedObj instanceof CFSpace ? (CFSpace) selectedObj : null); + } + } + refreshWizardUI(); + } + + protected void setSpaceInProperties(CFSpace selectedSpace) { + targetProperties.setSpace(selectedSpace); + } + + private void refreshWizardUI() { + if (getWizard() != null && getWizard().getContainer() != null) { + setPageComplete(targetProperties.getSpaceName() != null && targetProperties.getOrganizationName() != null); + + getWizard().getContainer().updateButtons(); + } + } + + static class SpacesSorter extends ViewerSorter { + + public SpacesSorter() { + + } + + public int compare(Viewer viewer, Object e1, Object e2) { + if (e1 instanceof CFEntity && e2 instanceof CFEntity) { + String name1 = ((CFEntity) e1).getName(); + String name2 = ((CFEntity) e2).getName(); + return name1.compareTo(name2); + } + + return super.compare(viewer, e1, e2); + } + + } + + class TableContentProvider implements ITreeContentProvider { + private Object[] elements; + + public TableContentProvider() { + } + + public void dispose() { + } + + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof CFOrganization) { + List children = spaces.getOrgSpaces(((CFOrganization) parentElement).getName()); + if (children != null) { + return children.toArray(new CFSpace[children.size()]); + } + } + return null; + } + + public Object[] getElements(Object inputElement) { + return elements; + } + + public Object getParent(Object element) { + return null; + } + + public boolean hasChildren(Object element) { + return false; + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + if (newInput instanceof Object[]) { + elements = (Object[]) newInput; + } + } + } + + static class SpacesLabelProvider extends LabelProvider { + + public SpacesLabelProvider() { + + } + + public String getText(Object element) { + if (element instanceof CFEntity) { + CFEntity cloudEntity = (CFEntity) element; + return cloudEntity.getName(); + } + return super.getText(element); + } + } + + @Override + public void gotValue(LiveExpression exp, ValidationResult value) { + value = exp.getValue(); + setErrorMessage(null); + canFinish = true; + if (value.status == IStatus.ERROR) { + setErrorMessage(value.msg); + canFinish = false; + } else { + setMessage(value.msg, value.status); + } + refreshWizardUI(); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/PasswordDialogModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/PasswordDialogModel.java new file mode 100644 index 000000000..b1bec70d4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/PasswordDialogModel.java @@ -0,0 +1,232 @@ +/******************************************************************************* + * Copyright (c) 2016, 2018 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.cf.dialogs; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFClientParams; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.CloudFoundryClientFactory; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials.LoginMethod; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryTargetProperties; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; +import org.springsource.ide.eclipse.commons.livexp.ui.OkButtonHandler; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +import reactor.core.publisher.Mono; + +/** + * Password dialog model. Provides ability to specify password and whether it + * needs to be stored. + * + * @author Alex Boyko + * @author Kris De Volder + */ +public class PasswordDialogModel implements OkButtonHandler { + + private static final ValidationResult REQUEST_VALIDATION_MESSAGE = ValidationResult.info( + "Click the 'Validate' button to verify the credentials."); + + private static final ValidationResult VALIDATION_IN_PROGRESS_MESSAGE = ValidationResult.info( + "Please wait. Contacting CF to verify the credentials..." + ); + + private CloudFoundryTargetProperties currentParams; + private CloudFoundryClientFactory clientFactory; + + private final LiveVariable refreshToken = new LiveVariable<>(); //This is set when credentials are succesfully validated. + private final LiveVariable needValidationRequest = new LiveVariable<>(true); + + private LiveVariable credentialsValidationResult; + final private LiveVariable fMethod; + final private LiveVariable fPasswordVar; + final private LiveVariable fStoreVar; + private boolean okButtonPressed = false; + private Validator passwordValidator; + private LiveExpression storeValidator; + + private void credentialsChangedHandler(LiveExpression exp, T value) { + needValidationRequest.setValue(true); + } + + public PasswordDialogModel(CloudFoundryClientFactory cfFactory, CloudFoundryTargetProperties currentParams, StoreCredentialsMode storeMode) { + super(); + this.clientFactory = cfFactory; + this.currentParams = currentParams; + fPasswordVar = new LiveVariable<>(""); + fStoreVar = new LiveVariable<>(storeMode); + fMethod = new LiveVariable<>(LoginMethod.PASSWORD); + storeValidator = makeStoreCredentialsValidator(getMethodVar(), fStoreVar); + credentialsValidationResult = new LiveVariable<>(REQUEST_VALIDATION_MESSAGE); + fMethod.addListener(this::credentialsChangedHandler); + fPasswordVar.addListener(this::credentialsChangedHandler); + } + + public String getUser() { + return currentParams.getUsername(); + } + + public String getTargetId() { + return CloudFoundryTargetProperties.getId(currentParams); + } + + public LiveVariable getPasswordVar() { + return fPasswordVar; + } + + public LiveVariable getStoreVar() { + return fStoreVar; + } + + public boolean isOk() { + return okButtonPressed; + } + + @Override + public void performOk() throws Exception { + okButtonPressed = true; + } + + public LiveExpression getPasswordValidator() { + if (passwordValidator==null) { + passwordValidator = new Validator() { + { + dependsOn(fPasswordVar); + dependsOn(needValidationRequest); + dependsOn(credentialsValidationResult); + } + @Override + protected ValidationResult compute() { + String pw = fPasswordVar.getValue(); + if (!StringUtil.hasText(pw)) { + return ValidationResult.error("Password can not be empty"); + } + if (needValidationRequest.getValue()) { + return REQUEST_VALIDATION_MESSAGE; + } + return credentialsValidationResult.getValue(); + } + }; + } + return passwordValidator; + } + + public LiveExpression getStoreValidator() { + return storeValidator; + } + + /** + * Determines the 'effective' StoreCredentialsMode. This may be different + * from what the user explicitly chose. If the user choice is 'invalid' + * we ignore it (with a warning) and replace it with STORE_NOTHING. + */ + public StoreCredentialsMode getEffectiveStoreMode() { + if (storeValidator.getValue().isOk()) { + return fStoreVar.getValue(); + } + return StoreCredentialsMode.STORE_NOTHING; + } + + public static Validator makeStoreCredentialsValidator(LiveExpression method, LiveExpression storeCredentials ) { + return new Validator() { + { + dependsOn(method); + dependsOn(storeCredentials); + } + + @Override + protected ValidationResult compute() { + if ( + method.getValue()==CFCredentials.LoginMethod.TEMPORARY_CODE && + storeCredentials.getValue()==StoreCredentialsMode.STORE_PASSWORD + ) { + return ValidationResult.warning("'Store Password' is useless for a 'Temporary Code'. This option will be ignored!"); + } + return ValidationResult.OK; + } + }; + } + + /** + * Validates credentials currently entered in the dialog fields, and update + * other dialog model elements in the process (validation result / status and + * refreshToken needed to produce effective credential object. + */ + public Mono validateCredentials() { + return validateCredentialsHelper(CFCredentials.fromLogin(this.fMethod.getValue(), this.fPasswordVar.getValue())) + .doOnSubscribe((e) -> { + needValidationRequest.setValue(false); + refreshToken.setValue(null); + credentialsValidationResult.setValue(VALIDATION_IN_PROGRESS_MESSAGE); + }) + .onErrorResume((e) -> Mono.just(ValidationResult.error(ExceptionUtil.getMessage(e)))) + .doOnNext((result) -> { + credentialsValidationResult.setValue(result); + }); + } + + /** + * Validates a given credential object and returns a validation result (asynchornously). + */ + private Mono validateCredentialsHelper(CFCredentials creds) { + + return Mono.defer(() -> { + if (!StringUtil.hasText(creds.getSecret())) { + //Don't bother verifying empty passwords. + return Mono.just(REQUEST_VALIDATION_MESSAGE); + } + CFClientParams params = new CFClientParams( + currentParams.getUrl(), + currentParams.getUsername(), + creds, + currentParams.isSelfsigned(), + null, null, + currentParams.skipSslValidation() + ); + ClientRequests client = clientFactory.getClient(params); + return client.getUserName() + .flatMap(actualUserName -> { + refreshToken.setValue(client.getRefreshToken()); + if (!currentParams.getUsername().equals(actualUserName)) { + return Mono.just(ValidationResult.error("The credentials belong to a different user!")); + } + return Mono.just(ValidationResult.OK); + }) + .doFinally(signal -> client.dispose()); + }); + } + + public LiveVariable getMethodVar() { + return fMethod; + } + + public CFCredentials getCredentials() { + String refreshToken = this.refreshToken.getValue(); + if (refreshToken==null) { + throw new IllegalStateException("Credentials must be validated before retrieving them from the model"); + } + //Produce credential object consistent with store credentials mode + StoreCredentialsMode storeMode = getEffectiveStoreMode(); + switch (storeMode) { + case STORE_NOTHING: + case STORE_TOKEN: + return CFCredentials.fromRefreshToken(refreshToken); + case STORE_PASSWORD: + return CFCredentials.fromPassword(fPasswordVar.getValue()); + default: + throw new IllegalStateException("Bug! Missing case?"); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/RunTargetWizard.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/RunTargetWizard.java new file mode 100644 index 000000000..0863c89ea --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/RunTargetWizard.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.dialogs; + +import org.eclipse.jface.wizard.Wizard; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; + +/** + * Creates a run target + * + */ +public class RunTargetWizard extends Wizard { + + private CloudFoundryTargetWizardPage page; + private CloudFoundryTargetWizardModel model; + + public RunTargetWizard(CloudFoundryTargetWizardModel model) { + this.model = model; + + setWindowTitle("Add a Run Target"); + + setNeedsProgressMonitor(true); + } + + @Override + public void addPages() { + // TODO: Turn into framework and load pages based on an initial page + // that shows different target types. + // Right it is hardcoded to load the Cloud Foundry target page. + + page = new CloudFoundryTargetWizardPage(model); + addPage(page); + } + + @Override + public boolean performFinish() { + return getRunTarget() != null; + } + + public CloudFoundryRunTarget getRunTarget() { + return page != null ? page.createRunTarget() : null; + } + + @Override + public boolean needsProgressMonitor() { + return true; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/StoreCredentialsMode.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/StoreCredentialsMode.java new file mode 100644 index 000000000..d1d9f47a2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/StoreCredentialsMode.java @@ -0,0 +1,150 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.dialogs; + +import java.util.EnumSet; + +import org.eclipse.core.runtime.Platform; +import org.eclipse.equinox.security.storage.StorageException; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModelContext; +import org.springframework.ide.eclipse.boot.dash.model.SecuredCredentialsStore; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.CannotAccessPropertyException; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.livexp.ui.Ilabelable; + +public enum StoreCredentialsMode implements Ilabelable { + + STORE_PASSWORD { + @Override + public String getLabel() { + return "Store Password"; + } + + @Override + public CFCredentials loadCredentials(BootDashModelContext context, RunTargetType type, String runTargetId) throws CannotAccessPropertyException { + try { + String password = context.getSecuredCredentialsStore().getCredentials(secureStoreScopeKey(type.getName(), runTargetId)); + if (password!=null) { + return CFCredentials.fromPassword(password); + } + return null; + } catch (StorageException e) { + throw new CannotAccessPropertyException("Failed to load credentials", e); + } + } + + @Override + protected void basicSaveCredentials(BootDashModelContext context, RunTargetType type, String runTargetId, CFCredentials credentials) throws CannotAccessPropertyException { + try { + String storedString = credentials.getSecret(); + context.getSecuredCredentialsStore().setCredentials(secureStoreScopeKey(type.getName(), runTargetId), storedString); + } catch (StorageException e) { + throw new CannotAccessPropertyException("Failed to save credentials", e); + } + } + + @Override + protected void eraseCredentials(BootDashModelContext context, RunTargetType type, String runTargetId) { + try { + SecuredCredentialsStore store = context.getSecuredCredentialsStore(); + //Be careful and avoid annoying password popup just to erase data in a locked secure store. + if (store.isUnlocked()) { + store.setCredentials(secureStoreScopeKey(type.getName(), runTargetId), null); + } + } catch (StorageException e) { + Log.log(e); + } + } + + private String secureStoreScopeKey(String targetTypeName, String targetId) { + return targetTypeName+":"+targetId; + } + }, + + STORE_TOKEN { + @Override + public String getLabel() { + return "Store OAuth Token"; + } + + private String privateStoreKey(String targetType, String targetId) { + return targetType+":"+targetId + ":token"; + } + + @Override + public CFCredentials loadCredentials(BootDashModelContext context, RunTargetType type, String runTargetId) { + String token = context.getPrivatePropertyStore().get(privateStoreKey(type.getName(), runTargetId)); + if (token!=null) { + return CFCredentials.fromRefreshToken(token); + } + return null; + } + + @Override + public void basicSaveCredentials(BootDashModelContext context, RunTargetType type, String runTargetId, CFCredentials credentials) throws CannotAccessPropertyException { + try { + String storedString = credentials.getSecret(); + context.getPrivatePropertyStore().put(privateStoreKey(type.getName(), runTargetId), storedString); + } catch (Exception e) { + throw new CannotAccessPropertyException("Failed to save credentials", e); + } + } + + @Override + protected void eraseCredentials(BootDashModelContext context, RunTargetType type, String runTargetId) { + try { + IPropertyStore store = context.getPrivatePropertyStore(); + store.put(privateStoreKey(type.getName(), runTargetId), null); + } catch (Exception e) { + Log.log(e); + } + } + }, + + STORE_NOTHING { + @Override + public String getLabel() { + return "Do NOT Store"; + } + + @Override + public CFCredentials loadCredentials(BootDashModelContext context, RunTargetType type, String runTargetId) { + return null; + } + + @Override + protected void basicSaveCredentials(BootDashModelContext context, RunTargetType type, String runTargetId, CFCredentials credentials) { + //nothing to do + } + + @Override + protected void eraseCredentials(BootDashModelContext context, RunTargetType type, String runTargetId) { + //nothing to do + } + }; + + public abstract CFCredentials loadCredentials(BootDashModelContext context, RunTargetType type, String runTargetId) throws CannotAccessPropertyException; + protected abstract void eraseCredentials(BootDashModelContext context, RunTargetType type, String runTargetId); + protected abstract void basicSaveCredentials(BootDashModelContext context, RunTargetType type, String runTargetId, CFCredentials credentials) throws CannotAccessPropertyException; + + public final void saveCredentials(BootDashModelContext context, RunTargetType type, String runTargetId, CFCredentials credentials) throws CannotAccessPropertyException { + for (StoreCredentialsMode mode : EnumSet.allOf(StoreCredentialsMode.class)) { + if (mode==this) { + mode.basicSaveCredentials(context, type, runTargetId, credentials); + } else { + mode.eraseCredentials(context, type, runTargetId); + } + } + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/UpdatePasswordDialog.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/UpdatePasswordDialog.java new file mode 100644 index 000000000..6989a9baf --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/dialogs/UpdatePasswordDialog.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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 + * + * Original from org.cloudfoundry.ide.eclipse.server.ui.internal + * and implemented under: + * + * Copyright (c) 2012, 2014 Pivotal Software, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.dialogs; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.Status; +import org.eclipse.swt.widgets.Shell; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials.LoginMethod; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.SelectionModel; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.ui.ButtonSection; +import org.springsource.ide.eclipse.commons.livexp.ui.ChooseOneSectionCombo; +import org.springsource.ide.eclipse.commons.livexp.ui.CommentSection; +import org.springsource.ide.eclipse.commons.livexp.ui.DialogWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.StringFieldSection; +import org.springsource.ide.eclipse.commons.livexp.ui.WizardPageSection; + +import reactor.core.scheduler.Schedulers; + +/** + * Dialog for setting the password and "store password" flag. + * + * @author Terry Denney + * @author Alex Boyko + * @author Kris De Volder + */ +public class UpdatePasswordDialog extends DialogWithSections { + + private static final StoreCredentialsMode[] storeCredentialChoices = EnumSet.allOf(StoreCredentialsMode.class) + .toArray(new StoreCredentialsMode[0]); + + private PasswordDialogModel model; + + public UpdatePasswordDialog(Shell parentShell, PasswordDialogModel model) { + super("Update Credentials", model, parentShell); + this.model = model; + disableOkButtonAt(Status.INFO); + } + + @Override + protected List createSections() throws CoreException { + List sections = new ArrayList<>(); + + sections.add(new CommentSection(this, + "The password/code must match your existing target credentials in " + model.getTargetId() + ".")); + + sections.add(new ChooseOneSectionCombo<>(this, "Method:", model.getMethodVar(), EnumSet.allOf(LoginMethod.class))); + sections.add(new StringFieldSection(this, "Password:", model.getPasswordVar(), model.getPasswordValidator()).setPassword(true)); + sections.add(storeCredentialsSection(this, model.getStoreVar(), model.getStoreValidator())); + sections.add(new ButtonSection(this, "Validate", () -> { + model.validateCredentials().subscribeOn(Schedulers.elastic()).subscribe(); + })); + + return sections; + } + + public static ChooseOneSectionCombo storeCredentialsSection(IPageWithSections owner, + LiveVariable storeCreds, + LiveExpression validator + ) { + return new ChooseOneSectionCombo<>(owner, "Remember:", new SelectionModel<>(storeCreds, validator), storeCredentialChoices); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/BootDashCfColumns.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/BootDashCfColumns.java new file mode 100644 index 000000000..e08e7fb64 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/BootDashCfColumns.java @@ -0,0 +1,7 @@ +package org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel; + +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; + +public class BootDashCfColumns { + public static final BootDashColumn JMX_SSH_TUNNEL = new BootDashColumn("JMX_SSH_TUNNEL"); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/CloudFoundryRemoteBootAppsDataContributor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/CloudFoundryRemoteBootAppsDataContributor.java new file mode 100644 index 000000000..e45ce4606 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/CloudFoundryRemoteBootAppsDataContributor.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel; + +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springsource.ide.eclipse.commons.boot.ls.remoteapps.RemoteBootAppsDataHolder; +import org.springsource.ide.eclipse.commons.boot.ls.remoteapps.RemoteBootAppsDataHolder.RemoteAppData; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +public class CloudFoundryRemoteBootAppsDataContributor implements RemoteBootAppsDataHolder.Contributor { + + private SimpleDIContext context; + + public CloudFoundryRemoteBootAppsDataContributor(SimpleDIContext context) { + this.context = context; + } + + @Override + public ObservableSet getRemoteApps() { + return context.getBean(JmxSshTunnelManager.class).getUrls(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/JmxSshTunnelManager.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/JmxSshTunnelManager.java new file mode 100644 index 000000000..116062d54 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/JmxSshTunnelManager.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2018 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.cf.jmxtunnel; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; + +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshTunnel; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; +import org.springsource.ide.eclipse.commons.boot.ls.remoteapps.RemoteBootAppsDataHolder.RemoteAppData; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +import com.google.common.collect.ImmutableSet; + +/** + * An instance of this class keeps track of open ssh tunnels for JMX connections to + * remote hosts. It is capable of producing a (Live)Set of JMX urls for all the + * open tunnels. + */ +public class JmxSshTunnelManager { + + private Map tunnels = new HashMap<>(); + + private ObservableSet jmxUrls = ObservableSet.builder() + .refresh(AsyncMode.ASYNC) + .compute(this::collectUrls) + .build(); + + private synchronized ImmutableSet collectUrls() { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (Entry entry : tunnels.entrySet()) { + SshTunnel tunnel = entry.getKey(); + CloudAppDashElement app = entry.getValue(); + int port = tunnel.getLocalPort(); + if (port>0) { + RemoteAppData data = new RemoteAppData(); + data.setJmxurl(JmxSupport.getJmxUrl(port)); + data.setHost(app.getLiveHost()); + data.setKeepChecking(false); + UUID guid = app.getAppGuid(); + if (guid!=null) { + data.setProcessId(guid.toString()); + } + builder.add(data); + } + } + return builder.build(); + } + + public void add(SshTunnel sshTunnel, CloudAppDashElement app) { + sshTunnel.onDispose(this::handleTunnelClosed); + tunnels.put(sshTunnel, app); + jmxUrls.refresh(); + app.getJmxSshTunnelStatus().refresh(); + } + + private void handleTunnelClosed(Disposable disposed) { + CloudAppDashElement owner = tunnels.remove(disposed); + owner.getJmxSshTunnelStatus().refresh(); + jmxUrls.refresh(); + } + + public ObservableSet getUrls() { + return jmxUrls; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/JmxSupport.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/JmxSupport.java new file mode 100644 index 000000000..299d70e5e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/jmxtunnel/JmxSupport.java @@ -0,0 +1,186 @@ +/******************************************************************************* + * Copyright (c) 2018 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.cf.jmxtunnel; + +import java.util.Map; + +import org.springframework.ide.eclipse.boot.dash.cf.client.SshClientSupport; +import org.springframework.ide.eclipse.boot.dash.cf.client.SshHost; +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshTunnel; +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshTunnelFactory; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.launch.util.PortFinder; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * Helper class providing functionality to connect to JMX on a remote spring boot app + * running on CF, using ssh tunneling. + *

+ * The main responsiblity of this class is to manage tunnel life-cycle based on the + * app's state. (I.e. ensure tunnel is created when app started and tunnel is closed + * when app stopped or deleted). + * + * @author Kris De Volder + */ +public class JmxSupport { + + private static final String JAVA_OPTS = "JAVA_OPTS"; + + private static final String JMX_OPTION_PAT = + "-D(com\\.sun\\.management\\.jmxremote|java\\.rmi\\.server|spring\\.jmx)\\.[a-z\\.]*=\\S*\\s*"; + + private static final String JMX_ARGS(int port) { + return "-Dcom.sun.management.jmxremote.ssl=false " + + "-Dcom.sun.management.jmxremote.authenticate=false " + + "-Dcom.sun.management.jmxremote.port="+port+" " + + "-Dcom.sun.management.jmxremote.rmi.port="+port+" " + + "-Djava.rmi.server.hostname=localhost " + + "-Dcom.sun.management.jmxremote.local.only=false "+ + "-Dspring.jmx.enabled=true"; + } + + private CloudAppDashElement app; + private LiveExpression tunnelPort; + + private SshTunnel sshTunnel; + private boolean disposed; + + private JmxSshTunnelManager tunnels; + private SshTunnelFactory tunnelFactory; + + public JmxSupport(CloudAppDashElement cde, JmxSshTunnelManager tunnels, SshTunnelFactory tunnelFactory) { + this.tunnelFactory = tunnelFactory; + this.app = cde; + this.tunnels = tunnels; + this.tunnelPort = new AsyncLiveExpression(null, "Update SSH JMX Tunnel for "+app.getName()) { + { + dependsOn(app.getBaseRunStateExp()); + } + + @Override + protected Integer compute() { + RunState runState = app.getBaseRunStateExp().getValue(); + if (runState == RunState.RUNNING || runState == RunState.DEBUGGING) { + if (app.getEnableJmxSshTunnel()) { + return app.getCfJmxPort(); + } + } + return -1; + } + }; + + this.tunnelPort.addListener((exp, v) -> { + Integer port = exp.getValue(); + if (port!=null && port>0) { + ensureTunnel(port); + } else { + closeTunnel(false); + } + }); + cde.onDispose(d -> closeTunnel(true)); + } + + private synchronized void ensureTunnel(Integer port) { + if (sshTunnel==null && !disposed) { + createSshTunnel(port); + } + } + + private synchronized void closeTunnel(boolean disposed) { + this.disposed = this.disposed || disposed; + if (sshTunnel!=null) { + sshTunnel.close(); + sshTunnel = null; + } + } + + public int getPort() { + int port = app.getCfJmxPort(); + if (port<=0) { + try { + //TODO: should we work harder at allocating a unique port accross all remote and local + // apps? Right now there is a small chance that two apps will get the same port. + port = PortFinder.findFreePort(); + app.setCfJmxPort(port); + } catch (Exception e) { + Log.log(e); + } + } + return port; + } + + public void setupEnvVars(Map env) { + int port = getPort(); + if (port>0) { + String javaOpts = env.get(JAVA_OPTS); + if (javaOpts!=null) { + //Erase old vars + javaOpts = javaOpts.replaceAll(JMX_OPTION_PAT, "").trim(); + } else { + javaOpts = ""; + } + String jmxArgs = JMX_ARGS(port); + if ("".equals(javaOpts)) { + // no other java opts yet + javaOpts = jmxArgs; + } else { + javaOpts = javaOpts + " " +jmxArgs; + } + env.put(JAVA_OPTS, javaOpts); + } + } + + private void createSshTunnel(Integer port) { + if (port!=null && port>0) { + try { + app.log("Fetching JMX SSH tunnel parameters..."); + SshClientSupport sshInfo = app.getTarget().getSshClientSupport(); + SshHost sshHost = sshInfo.getSshHost(); + String sshUser = sshInfo.getSshUser(app.getAppGuid(), 0); + String sshCode = sshInfo.getSshCode(); + int remotePort = port; + + app.log("JMX SSH tunnel parameters:"); + app.log(" host: "+sshHost); + app.log(" user: "+sshUser); + app.log(" code: "+sshCode); + app.log(" remote port: "+remotePort); + + //2: create tunnel + app.log("Creating JMX SSH tunnel..."); + this.sshTunnel = tunnelFactory.create(sshHost, sshUser, sshCode, remotePort, app, remotePort); + String jmxUrl = getJmxUrl(remotePort); + app.log("JMX SSH tunnel URL = "+jmxUrl); + tunnels.add(sshTunnel, app); + } catch (Exception e) { + app.setError(e); + app.log(ExceptionUtil.getMessage(e)); + Log.log(e); + } + } + } + + public static String getJmxUrl(Integer port) { + if (port!=null && port>0) { + return "service:jmx:rmi://localhost:"+port+"/jndi/rmi://localhost:"+port+"/jmxrmi"; + } + return null; + } + + public boolean isTunnelActive() { + SshTunnel sshTunnel = this.sshTunnel; + return sshTunnel != null && !sshTunnel.isDisposed(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/labels/BootDashCfLabels.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/labels/BootDashCfLabels.java new file mode 100644 index 000000000..b5cc488ce --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/labels/BootDashCfLabels.java @@ -0,0 +1,38 @@ +package org.springframework.ide.eclipse.boot.dash.cf.labels; + +import static org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.BootDashCfColumns.JMX_SSH_TUNNEL; +import static org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels.MUTED_TEXT_DECORATION_COLOR_THEME; +import static org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels.TEXT_DECORATION_COLOR_THEME; + +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.swt.graphics.Color; +import org.eclipse.ui.PlatformUI; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.JmxSshTunnelStatus; +import org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +import com.google.common.collect.ImmutableSet; + +public class BootDashCfLabels { + + public static final BootDashLabels.Contribution jmxDecoration = new BootDashLabels.Contribution(ImmutableSet.of(JMX_SSH_TUNNEL)) { + @Override public StyledString getStyledText(Stylers stylers, BootDashElement element, BootDashColumn col) { + if (element instanceof CloudAppDashElement) { + CloudAppDashElement cfApp = (CloudAppDashElement) element; + JmxSshTunnelStatus tunnelState = cfApp.getJmxSshTunnelStatus().getValue(); + if (tunnelState!=JmxSshTunnelStatus.DISABLED) { + Color activeColor = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getColorRegistry().get(TEXT_DECORATION_COLOR_THEME); + Color notActiveColor = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getColorRegistry().get(MUTED_TEXT_DECORATION_COLOR_THEME); + return new StyledString("jmx", tunnelState==JmxSshTunnelStatus.ACTIVE ? stylers.color(activeColor) : stylers.color(notActiveColor)); + } + return new StyledString(); + } + return null; + } + + }; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/ApplicationRunningStateTracker.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/ApplicationRunningStateTracker.java new file mode 100644 index 000000000..2279e0824 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/ApplicationRunningStateTracker.java @@ -0,0 +1,190 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.model; + +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFAppState; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplicationDetail; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFInstanceState; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFInstanceStats; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.DefaultClientRequestsV2; +import org.springframework.ide.eclipse.boot.dash.console.LogType; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +public class ApplicationRunningStateTracker { + // Give time for Diego-enabled apps with health check that may take a while to start + // Users can always manually stop the element if it is taking too long to check the run state of the element + public static final long APP_START_TIMEOUT = DefaultClientRequestsV2.APP_START_TIMEOUT.toMillis(); + + public static final long WAIT_TIME = 1000; + + private final ClientRequests requests; + + private final String appName; + + private final CloudFoundryBootDashModel model; + + private final long timeout; + + private final CancelationToken cancelationToken; + + private final CloudAppDashElement element; + + + public ApplicationRunningStateTracker(CancelationToken cancelationToken, CloudAppDashElement app) { + this.model = app.getBootDashModel(); + this.requests = model.getClient(); + this.appName = app.getName(); + this.timeout = APP_START_TIMEOUT; + this.cancelationToken = cancelationToken; + this.element = app; + } + + protected void checkTerminate(IProgressMonitor monitor) + throws OperationCanceledException { + this.element.checkTerminationRequested(cancelationToken, monitor); + } + + /** + * Polls cloudfoundry until element has succeeded or failed to start. Sending updates to console + * and return the final run state. + */ + public RunState startTracking(IProgressMonitor monitor) throws Exception, OperationCanceledException { + + // fetch an updated Cloud Application that reflects changes that + // were + // performed on it. Make sure the element element reference is updated + // as + // run state of the element depends on the element being up to date. + // Wait for application to be started + RunState runState = RunState.UNKNOWN; + + long currentTime = System.currentTimeMillis(); + long roughEstimateFetchStatsms = 5000; + + long totalTime = currentTime + timeout; + String checkingMessage = "Checking if the application is running"; + + int estimatedAttempts = (int) (timeout / (WAIT_TIME + roughEstimateFetchStatsms)); + + monitor.beginTask(checkingMessage, estimatedAttempts); + + model.getElementConsoleManager().writeToConsole(element, checkingMessage + ". Please wait...", + LogType.STDOUT); + + CFApplicationDetail app = requests.getApplication(appName); + + if (app == null) { + throw new OperationCanceledException(); + } + + // Get the guid, as it is more efficient for lookup + //UUID appGuid = element.getGuid(); + + while (runState != RunState.RUNNING && runState != RunState.FLAPPING && runState != RunState.CRASHED + && currentTime < totalTime) { + int timeLeft = (int) ((totalTime - currentTime) / 1000); + + // Don't log this. Only update the monitor + monitor.setTaskName(checkingMessage + ". Time left before timeout: " + timeLeft + 's'); + + checkTerminate(monitor); + + monitor.worked(1); + + runState = getRunState(app.getInstanceDetails()); + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException e) { + + } + + app = requests.getApplication(app.getName()); + // App no longer exists + if (app == null) { + throw new OperationCanceledException(); + } + + currentTime = System.currentTimeMillis(); + } + + if (runState != RunState.RUNNING) { + String warning = "Timed out waiting for application - " + appName + + " to start. Please wait and manually refresh the target, or check if the application logs show any errors."; + model.getElementConsoleManager().writeToConsole(this.element, warning, LogType.STDERROR); + throw ExceptionUtil.coreException(warning); + + } else { + model.getElementConsoleManager().writeToConsole(this.element, "Application appears to have started - " + appName, + LogType.STDOUT); + } + return runState; + } + + public static RunState getRunState(CFInstanceState instanceState) { + RunState runState = null; + if (instanceState != null) { + switch (instanceState) { + case RUNNING: + runState = RunState.RUNNING; + break; + case CRASHED: + runState = RunState.CRASHED; + break; + case FLAPPING: + runState = RunState.FLAPPING; + break; + case STARTING: + runState = RunState.STARTING; + break; + case DOWN: + runState = RunState.INACTIVE; + break; + default: + runState = RunState.UNKNOWN; + break; + } + } + + return runState; + } + + public static RunState getRunState(CFApplication app, List instances) { + + RunState runState = RunState.UNKNOWN; + // if element desired state is "Stopped", return inactive + if ((instances == null || instances.isEmpty()) + && app.getState() == CFAppState.STOPPED) { + runState = RunState.INACTIVE; + } else { + runState = getRunState(instances); + } + return runState; + } + + private static RunState getRunState(List stats) { + RunState runState = RunState.UNKNOWN; + if (stats!=null && !stats.isEmpty()) { + for (CFInstanceStats stat : stats) { + RunState instanceState = getRunState(stat.getState()); + runState = instanceState != null ? runState.merge(instanceState) : null; + } + } + return runState; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudAppDashElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudAppDashElement.java new file mode 100644 index 000000000..de46accd7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudAppDashElement.java @@ -0,0 +1,946 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.cf.model; + +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +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.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.api.Deletable; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplicationDetail; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFInstanceStats; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.HealthChecks; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFPushArguments; +import org.springframework.ide.eclipse.boot.dash.cf.debug.DebugSupport; +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshTunnelFactory; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.JmxSshTunnelManager; +import org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.JmxSupport; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement.CloudAppIdentity; +import org.springframework.ide.eclipse.boot.dash.cf.ops.CloudApplicationOperation; +import org.springframework.ide.eclipse.boot.dash.cf.ops.Operation; +import org.springframework.ide.eclipse.boot.dash.cf.ops.RemoteDevClientStartOperation; +import org.springframework.ide.eclipse.boot.dash.cf.ops.SetHealthCheckOperation; +import org.springframework.ide.eclipse.boot.dash.cf.routes.ParsedUri; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.JmxSshTunnelStatus; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.OperationTracker; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.OperationTracker.Task; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.console.ApplicationLogConsole; +import org.springframework.ide.eclipse.boot.dash.console.LegacyLogSource; +import org.springframework.ide.eclipse.boot.dash.console.LogType; +import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsUtil; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveDataCapableElement; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveDataConnectionManagementActions.ExecuteCommandAction; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; +import org.springframework.ide.eclipse.boot.dash.util.LogSink; +import org.springframework.ide.eclipse.boot.dash.util.Utils; +import org.springframework.ide.eclipse.boot.dash.views.BootDashModelConsoleManager; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import reactor.core.Disposable; + +/** + * A handle to a Cloud application. NOTE: This element should NOT hold Cloud + * application state as it may be discarded and created multiple times for the + * same app for any reason. + *

+ * Cloud application state should always be resolved from external sources + */ +public class CloudAppDashElement extends CloudDashElement implements BootDashElement, Deletable, LogSink, LiveDataCapableElement, LegacyLogSource { + + private static final boolean DEBUG = + (""+Platform.getLocation()).contains("kdvolder"); +// (""+Platform.getLocation()).contains("bamboo"); + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + static final private String DEPLOYMENT_MANIFEST_FILE_PATH = "deploymentManifestFilePath"; //$NON-NLS-1$ + private static final String PROJECT_NAME = "PROJECT_NAME"; + private static final String CF_JMX_PORT = "CF_JMX_PORT"; + private static final String CF_JMX_ENABLED = "CF_JMX_ENABLED"; + + private CancelationTokens cancelationTokens; + + private final CloudFoundryBootDashModel cloudModel; + private PropertyStoreApi persistentProperties; + + private final LiveVariable error = new LiveVariable<>(); + private final OperationTracker startOperationTracker = new OperationTracker(()-> this.getName(),error); + + private final LiveVariable appData = new LiveVariable<>(); + private final LiveVariable> instanceData = new LiveVariable<>(); + private final LiveExpression baseRunState = new LiveExpression() { + + { + dependsOn(appData); + dependsOn(instanceData); + dependsOn(startOperationTracker.inProgress); + dependsOn(error); + } + + @Override + protected RunState compute() { + debug("Compute baseRunState for "+CloudAppDashElement.this+" ..."); + if (error.getValue()!=null) { + debug("error.getValue() => "+error.getValue()); + debug("baseRunState for "+CloudAppDashElement.this+" => UNKNOWN"); + return RunState.UNKNOWN; + } + if (startOperationTracker.inProgress.getValue()>0) { + debug("startOperationTracker.inProgress.getValue() => "+startOperationTracker.inProgress.getValue()); + debug("baseRunState for "+CloudAppDashElement.this+" => STARTING"); + return RunState.STARTING; + } + CFApplication app = appData.getValue(); + debug("appData => "+app); + List instances = instanceData.getValue(); + debug("instances.size() => "+(instances==null?null:instances.size())); + if (instances!=null && app!=null) { + RunState rs = ApplicationRunningStateTracker.getRunState(app, instances); + debug("baseRunState from instances => "+rs); + return rs; + } + debug("baseRunState for "+CloudAppDashElement.this+" => UNKNOWN"); + return RunState.UNKNOWN; + } + }; + + /** + * Used as a temporary override of health-check info fetched from CF. This is cleared when element gets 'proper' + * data fetched from CF. This exists so that we can implement 'setHealthCheck' which is called to update + * model state after changing the health-check value individually. It makes sense in that case to only update + * this one bit of the model state rather than refresh all the data from CF. + */ + private final LiveVariable healthCheckOverride = new LiveVariable<>(); + private JmxSupport jmxSupport; + private LiveExpression jmxSshTunnelStatus = new LiveExpression() { + + //This liveexp is refreshed calls to its 'refresh' method + // - from jmxSshTunnelManager whenever a tunnel is created or disposed. + // - from setEnableJmxSshTunnel method. + + @Override + protected JmxSshTunnelStatus compute() { + if (getEnableJmxSshTunnel()) { + return jmxSupport!=null && jmxSupport.isTunnelActive() + ? JmxSshTunnelStatus.ACTIVE + : JmxSshTunnelStatus.INACTIVE; + } else { + return JmxSshTunnelStatus.DISABLED; + } + } + }; + private LiveExpression activeJmxUrl = new LiveExpression() { + { + dependsOn(jmxSshTunnelStatus); + } + + @Override + protected String compute() { + JmxSshTunnelStatus status = jmxSshTunnelStatus.getValue(); + if (status==JmxSshTunnelStatus.ACTIVE) { + return JmxSupport.getJmxUrl(jmxSupport.getPort()); + } + return null; + } + }; + + { + appData.addListener((e, v) -> { + healthCheckOverride.setValue(null); + }); + } + + protected void showConsole() { + try { + getBootDashModel().getElementConsoleManager().showConsole(this); + } catch (Exception e) { + Log.log(e); + } + } + + protected void resetAndShowConsole() { + try { + getBootDashModel().getElementConsoleManager().resetConsole(this); + getBootDashModel().getElementConsoleManager().showConsole(this); + } catch (Exception e) { + Log.log(e); + } + } + + public CloudAppDashElement(CloudFoundryBootDashModel model, String appName, IPropertyStore modelStore) { + super(model, new CloudAppIdentity(appName, model.getRunTarget())); + this.cancelationTokens = new CancelationTokens(); + this.cloudModel = model; + IPropertyStore backingStore = PropertyStores.createSubStore("A"+getName(), modelStore); + this.persistentProperties = PropertyStores.createApi(backingStore); + addElementNotifier(baseRunState); + addElementNotifier(appData); + addElementNotifier(healthCheckOverride); + addElementNotifier(jmxSshTunnelStatus); + this.addDisposableChild(baseRunState); + getJmxSupport(); //Must force creation of jmxSupport object, if applicable, otherwise it's runstate listener will not be + // active to start JmxSshTunnel. See bug: https://www.pivotaltracker.com/story/show/159376406 + jmxSshTunnelStatus.refresh(); + } + + @Override + public CloudFoundryBootDashModel getBootDashModel() { + return (CloudFoundryBootDashModel) super.getBootDashModel(); + } + + public void stopAsync() throws Exception { + cancelOperations(); + String appName = getName(); + getBootDashModel().runAsynch("Stopping application " + appName, appName, (IProgressMonitor monitor) -> { + stop(createCancelationToken(), monitor); + }, ui()); + } + + @Override + public void stop() throws Exception { + cancelOperations(); + stop(createCancelationToken(), new NullProgressMonitor()); + } + + public void stop(CancelationToken cancelationToken, IProgressMonitor monitor) throws Exception { + checkTerminationRequested(cancelationToken, monitor); + getClient().stopApplication(getName()); + refresh(); + } + + @Override + public void restart(RunState runningOrDebugging, UserInteractions ui) throws Exception { + Job job = new Job("Restarting " + getName()) { + @Override + protected IStatus run(IProgressMonitor _monitor) { + CancelationToken cancelationToken = cancelationTokens.create(); + try { + startOperationTracker.whileExecuting(ui, cancelationToken, _monitor, () -> { + cancelationTokens.cancelAllBefore(cancelationToken); + //Caution! It is important that canceling older tokens is done *inside* the 'whileExecuting'. + //Otherwise currently executing operations will exit before the restart operation is registered + //as 'in progress' which causes a brief flash in the runstate where it incorrectly registers as + // 'running' while it should be registering as 'starting'. + //See: https://www.pivotaltracker.com/story/show/159639098/comments/193569587 + cloudModel.runSynch("Restarting, goal state: " + runningOrDebugging, getName(), (IProgressMonitor monitor) -> { + if (getProject() != null) { + // Let push and debug resolve deployment properties + CloudApplicationDeploymentProperties deploymentProperties = null; + pushAndDebug(deploymentProperties, runningOrDebugging, ui, cancelationToken, monitor); + } else { + restartOnly(ui, cancelationToken, monitor); + } + }, ui); + }); + } catch (Exception e) { + Log.log(e); + } + return Status.OK_STATUS; + } + }; + job.setSystem(true); + job.schedule(); + } + + public DebugSupport getDebugSupport() { + //In the future we may need to choose between multiple strategies here. + return getBootDashModel().getDebugSupport(); + } + + public BootDashViewModel getViewModel() { + return getBootDashModel().getViewModel(); + } + + public void restartWithRemoteClient(UserInteractions ui, CancelationToken cancelationToken) { + String opName = "Restart Remote DevTools Client for application '" + getName() + "'"; + getBootDashModel().runAsynch(opName, getName(), (IProgressMonitor monitor) -> { + doRestartWithRemoteClient(RunState.RUNNING, ui, cancelationToken, monitor); + }, ui); + } + + protected void doRestartWithRemoteClient(RunState runningOrDebugging, UserInteractions ui, CancelationToken cancelationToken, IProgressMonitor monitor) + throws Exception { + + CloudFoundryBootDashModel model = getBootDashModel(); + Map envVars = model.getRunTarget().getClient().getApplicationEnvironment(getName()); + + if (getProject() == null) { + ExceptionUtil.coreException(new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, + "Local project not associated to CF app '" + getName() + "'")); + } + + new SetHealthCheckOperation(this, HealthChecks.HC_PROCESS, ui, /* confirmChange */true, cancelationToken) + .run(monitor); + + if (!DevtoolsUtil.isEnvVarSetupForRemoteClient(envVars, DevtoolsUtil.getSecret(getProject()))) { + // Let the push and debug operation resolve default properties + CloudApplicationDeploymentProperties deploymentProperties = null; + + pushAndDebug(deploymentProperties , runningOrDebugging, ui, cancelationToken, monitor); + /* + * Restart and push op resets console anyway, no need to reset it + * again + */ + } else if (getRunState() == RunState.INACTIVE) { + restartOnly(ui, cancelationToken, monitor); + } + + new RemoteDevClientStartOperation(model, getName(), runningOrDebugging, cancelationToken).run(monitor); + } + + + public void restartOnly(UserInteractions ui, CancelationToken cancelationToken, IProgressMonitor monitor) throws Exception { + whileStarting(ui, cancelationToken, monitor, () -> { + if (!getClient().applicationExists(getName())) { + throw ExceptionUtil.coreException( + "Unable to start the application. Application does not exist anymore in Cloud Foundry: " + + getName()); + } + + checkTerminationRequested(cancelationToken, monitor); + + log("Starting application: " + getName()); + getClient().restartApplication(getName(), CancelationTokens.merge(cancelationToken, monitor)); + + new ApplicationRunningStateTracker(cancelationToken, this).startTracking(monitor); + + CFApplicationDetail updatedInstances = getClient().getApplication(getName()); + setDetailedData(updatedInstances); + }); + } + + public void restartOnlyAsynch(UserInteractions ui, CancelationToken cancelationToken) { + String opName = "Restarting application " + getName(); + getBootDashModel().runAsynch(opName, getName(), (IProgressMonitor monitor) -> { + restartOnly(ui, cancelationToken, monitor); + }, ui); + } + + public void pushAndDebug(CloudApplicationDeploymentProperties deploymentProperties, RunState runningOrDebugging, + UserInteractions ui, CancelationToken cancelationToken, IProgressMonitor monitor) throws Exception { + String opName = "Starting application '" + getName() + "' in " + + (runningOrDebugging == RunState.DEBUGGING ? "DEBUG" : "RUN") + " mode"; + DebugSupport debugSupport = getDebugSupport(); + + if (runningOrDebugging == RunState.DEBUGGING) { + + if (debugSupport != null && debugSupport.isSupported(this)) { + Operation debugOp = debugSupport.createOperation(this, opName, ui, cancelationToken); + + push(deploymentProperties, runningOrDebugging, debugSupport, cancelationToken, ui, monitor); + debugOp.run(monitor); + } else { + String title = "Debugging is not supported for '" + getName() + "'"; + String msg = debugSupport.getNotSupportedMessage(this); + if (msg == null) { + msg = title; + } + ui.errorPopup(title, msg); + throw ExceptionUtil.coreException(msg); + } + } else { + push(deploymentProperties, runningOrDebugging, debugSupport, cancelationToken, ui, monitor); + } + } + + @Override + public void openConfig(UserInteractions ui) { + + } + + @Override + public String getName() { + return delegate.getAppName(); + } + + /** + * Returns the project associated with this element or null. If includeNonExistingProjects is + * true, then the project is returned even it no longer exists. + */ + public IProject getProject(boolean includeNonExistingProjects) { + String name = getPersistentProperties().get(PROJECT_NAME); + if (name!=null) { + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(name); + if (includeNonExistingProjects || project.exists()) { + return project; + } + } + return null; + } + + /** + * Returns the remote jmx port that should/will be used when deploying this + * app to CF (so that we can SSH tunnel to that port to attach to JMX on the remote app). + */ + public int getCfJmxPort() { + return getPersistentProperties().get(CF_JMX_PORT, -1); + } + + public void setCfJmxPort(int port) throws Exception { + getPersistentProperties().put(CF_JMX_PORT, port>0 ? ""+port : null); + } + + public LiveExpression getJmxSshTunnelStatus() { + return jmxSshTunnelStatus; + } + + public boolean getEnableJmxSshTunnel() { + return getPersistentProperties().get(CF_JMX_ENABLED, false); + } + + public void setEnableJmxSshTunnel(boolean enable) throws Exception { + boolean old = getEnableJmxSshTunnel(); + getPersistentProperties().put(CF_JMX_ENABLED, enable); + jmxSshTunnelStatus.refresh(); + } + + + /** + * Returns the project associated with this element or null. The project returned is + * guaranteed to exist. + */ + @Override + public IProject getProject() { + return getProject(false); + } + + /** + * Set the project 'binding' for this element. + * @return true if the element was changed by this operation. + */ + public boolean setProject(IProject project) { + try { + PropertyStoreApi props = getPersistentProperties(); + String oldValue = props.get(PROJECT_NAME); + String newValue = project==null?null:project.getName(); + if (!Objects.equals(oldValue, newValue)) { + props.put(PROJECT_NAME, newValue); + return true; + } + return false; + } catch (Exception e) { + Log.log(e); + return false; + } + } + + @Override + public RunState getRunState() { + RunState state = baseRunState.getValue(); + if (state == RunState.RUNNING) { + DebugSupport debugSupport = getDebugSupport(); + if (debugSupport.isDebuggerAttached(CloudAppDashElement.this)) { +// if (DevtoolsUtil.isDevClientAttached(this, ILaunchManager.DEBUG_MODE)) { + state = RunState.DEBUGGING; + } + } + return state; + } + + /** + * This method is mostly meant just for test purposes. The 'baseRunState' is really + * part of how this class internally computes runstate. Clients should have no business + * using it separate from the runtstate. + */ + public LiveExpression getBaseRunStateExp() { + return baseRunState; + } + + @Override + public int getLivePort() { + return 443; + } + + @Override + public String getLiveHost() { + CFApplication app = getSummaryData(); + if (app != null) { + List uris = app.getUris(); + if (uris != null) { + for (String _uri : uris) { + if (StringUtil.hasText(_uri)) { + ParsedUri uri = new ParsedUri(_uri); + return uri.getHostAndDomain(); + } + } + } + } + return null; + } + + @Override + public String getUrl() { + CFApplication app = getSummaryData(); + if (app != null) { + List uris = app.getUris(); + if (uris != null) { + for (String uri : uris) { + if (StringUtil.hasText(uri)) { + return Utils.pathJoin("https://"+uri, getDefaultRequestMappingPath()); + } + } + } + } + return null; + } + + public Integer getMemory() { + CFApplication app = getSummaryData(); + if (app != null) { + return app.getMemory(); + } + return null; + } + + public String getHealthCheckHttpEndpoint() { + CFApplication app = getSummaryData(); + if (app != null) { + return app.getHealthCheckHttpEndpoint(); + } + return null; + } + + public CFApplication getSummaryData() { + return appData.getValue(); + } + + @Override + public ILaunchConfiguration getActiveConfig() { + return null; + } + + @Override + public int getActualInstances() { + CFApplication data = getSummaryData(); + return data != null ? data.getRunningInstances() : 0; + } + + @Override + public int getDesiredInstances() { + CFApplication data = getSummaryData(); + return data != null ? data.getInstances() : 0; + } + + public String getHealthCheck() { + String hc = healthCheckOverride.getValue(); + if (hc!=null) { + return hc; + } + CFApplication data = getSummaryData(); + return data!=null ? data.getHealthCheckType() : DeploymentProperties.DEFAULT_HEALTH_CHECK_TYPE; + } + + /** + * Changes the cached health-check value for this model element. Note that this + * doesnt *not* change the real value of the health-check. + */ + public void setHealthCheck(String hc) { + this.healthCheckOverride.setValue(hc); + } + + + public UUID getAppGuid() { + CFApplication app = getSummaryData(); + if (app!=null) { + return app.getGuid(); + } + return null; + } + + @Override + public PropertyStoreApi getPersistentProperties() { + return persistentProperties; + } + + static class CloudAppIdentity { + + private final String appName; + private final RunTarget runTarget; + + @Override + public String toString() { + return appName + "@" + runTarget; + }; + + CloudAppIdentity(String appName, RunTarget runTarget) { + this.appName = appName; + this.runTarget = runTarget; + } + + public String getAppName() { + return this.appName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((appName == null) ? 0 : appName.hashCode()); + result = prime * result + ((runTarget == null) ? 0 : runTarget.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CloudAppIdentity other = (CloudAppIdentity) obj; + if (appName == null) { + if (other.appName != null) + return false; + } else if (!appName.equals(other.appName)) + return false; + if (runTarget == null) { + if (other.runTarget != null) + return false; + } else if (!runTarget.equals(other.runTarget)) + return false; + return true; + } + + } + + @Override + public void log(String message) { + log(message, LogType.STDOUT); + } + + public void log(String message, LogType logType) { + try { + getBootDashModel().getElementConsoleManager().writeToConsole(this, message, logType); + } catch (Exception e) { + Log.log(e); + } + } + + @Override + public BootDashModel getParent() { + return getBootDashModel(); + } + + @Override + protected LiveExpression getActuatorUrl() { + return activeJmxUrl; + } + + public IFile getDeploymentManifestFile() { + String text = getPersistentProperties().get(DEPLOYMENT_MANIFEST_FILE_PATH); + try { + return text == null ? null : ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(text)); + } catch (IllegalArgumentException e) { + Log.log(e); + return null; + } + } + + public void setDeploymentManifestFile(IFile file) { + try { + if (file == null) { + getPersistentProperties().put(DEPLOYMENT_MANIFEST_FILE_PATH, (String) null); + } else { + getPersistentProperties().put(DEPLOYMENT_MANIFEST_FILE_PATH, file.getFullPath().toString()); + } + } catch (Exception e) { + Log.log(e); + } + } + + public void setDetailedData(CFApplicationDetail appDetails) { + if (appDetails!=null) { + this.appData.setValue(appDetails); + this.instanceData.setValue(appDetails.getInstanceDetails()); + } else { + this.appData.setValue(null); + this.instanceData.setValue(null); + } + } + + public List getInstanceData() { + return this.instanceData.getValue(); + } + + public void setError(Throwable t) { + error.setValue(t); + } + + @Override + public CancelationToken createCancelationToken() { + return cancelationTokens.create(); + } + + @Override + public void cancelOperations() { + cancelationTokens.cancelAll(); + } + + /** + * Print a message to the console associated with this application. + */ + public void print(String msg) { + print(msg, LogType.STDOUT); + } + + /** + * Print a message to the console associated with this application. + */ + public void printError(String string) { + print(string, LogType.STDERROR); + } + + /** + * Print a message to the console associated with this application. + */ + public void print(String msg, LogType type) { + try { + BootDashModelConsoleManager consoles = getBootDashModel().getElementConsoleManager(); + consoles.writeToConsole(this, msg+"\n", type); + } catch (Exception e) { + Log.log(e); + } + } + + /** + * Attempt to refresh the data associated with this app in the model. Returns the + * refreshed element if this was succesful, null if the element was deleted (because during the + * refresh we discovered it not longer exists) and if something failed trying to refresh the element. + */ + public CloudAppDashElement refresh() throws Exception { + debug("Refreshing element: "+this.getName()); + CFApplicationDetail data = getClient().getApplication(getName()); + if (data==null) { + //Looks like element no longer exist in CF so remove it from the model + CloudFoundryBootDashModel model = getBootDashModel(); + model.removeApplication(getName()); + return null; + } + getBootDashModel().updateApplication(data); + return this; + } + + @Override + public CloudFoundryRunTarget getTarget() { + return (CloudFoundryRunTarget) super.getTarget(); + } + + private ClientRequests getClient() { + return getTarget().getClient(); + } + + public void push(CloudApplicationDeploymentProperties deploymentProperties, RunState runningOrDebugging, + DebugSupport debugSupport, CancelationToken cancelationToken, UserInteractions ui, IProgressMonitor monitor) + throws Exception { + + boolean isDebugging = runningOrDebugging == RunState.DEBUGGING; + whileStarting(ui, cancelationToken, monitor, () -> { + // Refresh app data and check that the application (still) exists in + // Cloud Foundry + // This also ensures that the 'diff change dialog' will pick up on + // the latest changes. + // TODO: should this refresh be moved closer to the where we + // actually compute the diff? + CloudAppDashElement updatedApp = this.refresh(); + if (updatedApp == null) { + ExceptionUtil.coreException(new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, + "No Cloud Application found for '" + getName() + "'")); + } + IProject project = getProject(); + if (project == null) { + ExceptionUtil.coreException(new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, + "Local project not associated to CF app '" + getName() + "'")); + } + + checkTerminationRequested(cancelationToken, monitor); + + CloudApplicationDeploymentProperties properties = deploymentProperties == null + ? getBootDashModel().resolveDeploymentProperties(updatedApp, ui, monitor) : deploymentProperties; + + // Update JAVA_OPTS env variable with Remote DevTools Client secret + DevtoolsUtil.setupEnvVarsForRemoteClient(properties.getEnvironmentVariables(), + DevtoolsUtil.getSecret(project)); + JmxSupport jmxSupport = getJmxSupport(); + if (jmxSupport!=null) { + jmxSupport.setupEnvVars(properties.getEnvironmentVariables()); + } + if (debugSupport != null) { + if (isDebugging) { + debugSupport.setupEnvVars(properties.getEnvironmentVariables()); + } else { + debugSupport.clearEnvVars(properties.getEnvironmentVariables()); + } + } + + checkTerminationRequested(cancelationToken, monitor); + + CFPushArguments pushArgs = properties.toPushArguments(getBootDashModel().getCloudDomains(monitor)); + + getClient().push(pushArgs, CancelationTokens.merge(cancelationToken, monitor)); + + log("Application pushed to Cloud Foundry: " + getName()); + }); + } + + public void whileStarting(UserInteractions ui, CancelationToken cancelationToken, IProgressMonitor monitor, Task task) throws Exception { + resetAndShowConsole(); + startOperationTracker.whileExecuting(ui, cancelationToken, monitor, task); + refresh(); + } + + public void checkTerminationRequested(CancelationToken cancelationToken, IProgressMonitor mon) + throws OperationCanceledException { + if (mon != null && mon.isCanceled() || cancelationToken != null && cancelationToken.isCanceled()) { + throw new OperationCanceledException(); + } + } + + @Override + public void delete() { + CloudFoundryBootDashModel model = getBootDashModel(); + CloudAppDashElement cloudElement = this; + cloudElement.cancelOperations(); + CancelationToken cancelToken = cloudElement.createCancelationToken(); + CloudApplicationOperation operation = new CloudApplicationOperation("Deleting: " + cloudElement.getName(), model, + cloudElement.getName(), cancelToken) { + + @Override + protected void doCloudOp(IProgressMonitor monitor) throws Exception, OperationCanceledException { + // Delete from CF first. Do it outside of synch block to avoid + // deadlock + model.getRunTarget().getClient().deleteApplication(appName); + model.getElementConsoleManager().terminateConsole(cloudElement); + model.removeApplication(cloudElement.getName()); + cloudElement.setProject(null); + } + }; + + // Allow deletions to occur concurrently with any other application + // operation + operation.setSchedulingRule(null); + getBootDashModel().runAsynch(operation, ui()); + } + + private UserInteractions ui() { + return injections().getBean(UserInteractions.class); + } + + @Override + public EnumSet supportedGoalStates() { + return CloudFoundryRunTarget.RUN_GOAL_STATES; + } + + public synchronized JmxSupport getJmxSupport() { + if (jmxSupport == null && getProject()!=null && getEnableJmxSshTunnel()) { + this.jmxSupport = new JmxSupport(this, + injections().getBean(JmxSshTunnelManager.class), + injections().getBean(SshTunnelFactory.class) + ); + } + return jmxSupport; + } + + private SimpleDIContext injections() { + return this.getViewModel().getContext().injections; + } + + public String getJmxUrl() { + int port = getCfJmxPort(); + if (port>0) { + return JmxSupport.getJmxUrl(port); + } + return null; + } + + @Override + public ImageDescriptor getRunStateImageDecoration() { + if (this.getTarget() != null && this.getRunState() == RunState.RUNNING) { + if (DevtoolsUtil.isDevClientAttached(this, ILaunchManager.RUN_MODE)) { + return BootDashActivator.getDefault().getImageRegistry().getDescriptor(BootDashActivator.DT_ICON_ID); + } + } + return null; + } + + @Override + public boolean matchesLiveProcessCommand(ExecuteCommandAction action) { + UUID appGuid = getAppGuid(); + return appGuid!=null && appGuid.toString().equals(action.getProcessId()); + } + + @Override + public Image getPropertiesTitleIconImage() { + return BootDashActivator.getDefault().getImageRegistry().get(BootDashActivator.CLOUD_ICON); + } + + @Override + public Disposable connectLog(ApplicationLogConsole logConsole) { + if (logConsole.getLogStreamingToken() == null) { + try { + ClientRequests client = getClient(); + return client.streamLogs(getName(), logConsole); + } catch (Exception e) { + logConsole.writeApplicationLog("Failed to stream contents from "+getTarget().getDisplayName()+" due to: " + e.getMessage(), + LogType.STDERROR); + } + } + return null; + } + + @Override + public String getProtocol() { + return "https"; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudDashApplications.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudDashApplications.java new file mode 100644 index 000000000..fad0dc9ee --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudDashApplications.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.model; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.ide.eclipse.boot.dash.livexp.DisposingFactory; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableSet; + +/** + * An instance of this class is responsible for managing a list of {@link CloudAppDashElement}. This means: + *

+ *

    + *
  • calling 'dispose' on any no longer needed elements. + *
  • ensuring that only a single object exists to represent an element with a given identity. + *
  • creating the elements as needed. + *
+ * + * @author Kris De Volder + * @author Nieraj Singh + */ +public class CloudDashApplications extends AbstractDisposable { + + private static final boolean DEBUG = false; +// ("" + Platform.getLocation()).contains("bamboo") +// || ("" + Platform.getLocation()).contains("kdvolder"); + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + private final LiveSetVariable appNames = new LiveSetVariable<>(AsyncMode.SYNC); + private final ObservableSet applications; + private final DisposingFactory factory; + + public CloudDashApplications(final CloudFoundryBootDashModel model) { + factory = new DisposingFactory(appNames) { + @Override + protected CloudAppDashElement create(String appName) { + return new CloudAppDashElement(model, appName, model.getPropertyStore()); + } + }; + applications = org.springsource.ide.eclipse.commons.livexp.core.LiveSets.map(appNames, AsyncMode.SYNC, AsyncMode.ASYNC, new Function() { + @Override + public CloudAppDashElement apply(String appName) { + return factory.createOrGet(appName); + } + }); + addDisposableChild(factory); + addDisposableChild(applications); + if (DEBUG) { + applications.addListener((e, v) -> { + debug("applications change event!"); + debug(" event values = "+getNames(v)); + debug(" current values = "+getNames(applications.getValues())); + }); + } + } + + private List getNames(ImmutableSet v) { + return v.stream().map(CloudAppDashElement::getName).collect(Collectors.toList()); + } + + public void setAppNames(Collection names) { + appNames.replaceAll(names); + } + + public ObservableSet getApplications() { + return applications; + } + + public ImmutableSet getApplicationValues() { + return applications.getValues(); + } + + public CloudAppDashElement getApplication(String appName) { + return factory.createOrGet(appName); + } + + public CloudAppDashElement addApplication(String name) { + appNames.add(name); + return factory.createOrGet(name); + } + + public void removeApplication(String name) { + appNames.remove(name); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudDashElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudDashElement.java new file mode 100644 index 000000000..db66a6343 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudDashElement.java @@ -0,0 +1,133 @@ +/******************************************************************************* + * Copyright (c) 2017, 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.cf.model; + +import java.util.List; + +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansModel; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.Failable; +import org.springframework.ide.eclipse.boot.dash.model.MissingLiveInfoMessages; +import org.springframework.ide.eclipse.boot.dash.model.WrappingBootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.actuator.ActuatorClient; +import org.springframework.ide.eclipse.boot.dash.model.actuator.JMXActuatorClient; +import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.LiveEnvModel; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +import com.google.common.collect.ImmutableList; + +public abstract class CloudDashElement extends WrappingBootDashElement { + + public CloudDashElement(BootDashModel bootDashModel, T delegate) { + super(bootDashModel, delegate); + } + + private LiveExpression>> liveRequestMappings; + private LiveExpression> liveBeans; + private LiveExpression> liveEnv; + + @Override + public Failable> getLiveRequestMappings() { + synchronized (this) { + if (liveRequestMappings==null) { + final LiveExpression actuatorUrl = getActuatorUrl(); + liveRequestMappings = new AsyncLiveExpression>>(Failable.error(MissingLiveInfoMessages.NOT_YET_COMPUTED), "Fetch request mappings for '"+getName()+"'") { + @Override + protected Failable> compute() { + String target = actuatorUrl.getValue(); + if (target!=null) { + ActuatorClient client = JMXActuatorClient.forUrl(getTypeLookup(), () -> target); + List list = client.getRequestMappings(); + if (list!=null) { + return Failable.of(ImmutableList.copyOf(client.getRequestMappings())); + } + } + return Failable.error(getBootDashModel().getRunTarget().getType().getMissingLiveInfoMessages().getMissingInfoMessage(getName(), "mappings")); + } + + }; + liveRequestMappings.dependsOn(actuatorUrl); + addElementState(liveRequestMappings); + addDisposableChild(liveRequestMappings); + } + } + return liveRequestMappings.getValue(); + } + + @Override + public Failable getLiveBeans() { + synchronized (this) { + if (liveBeans == null) { + final LiveExpression actuatorUrl = getActuatorUrl(); + liveBeans = new AsyncLiveExpression>(Failable.error(MissingLiveInfoMessages.NOT_YET_COMPUTED), "Fetch beans for '"+getName()+"'") { + @Override + protected Failable compute() { + String target = actuatorUrl.getValue(); + if (target != null) { + ActuatorClient client = JMXActuatorClient.forUrl(getTypeLookup(), () -> target); + LiveBeansModel beans = client.getBeans(); + if (beans != null) { + return Failable.of(beans); + } + } + return Failable.error(getBootDashModel().getRunTarget().getType().getMissingLiveInfoMessages().getMissingInfoMessage(getName(), "beans")); + } + + }; + liveBeans.dependsOn(actuatorUrl); + addElementState(liveBeans); + addDisposableChild(liveBeans); + } + } + return liveBeans.getValue(); + } + + @Override + public Failable getLiveEnv() { + synchronized (this) { + if (liveEnv == null) { + final LiveExpression actuatorUrl = getActuatorUrl(); + liveEnv = new AsyncLiveExpression>(Failable.error(MissingLiveInfoMessages.NOT_YET_COMPUTED), "Fetch env for '"+getName()+"'") { + @Override + protected Failable compute() { + String target = actuatorUrl.getValue(); + if (target != null) { + ActuatorClient client = JMXActuatorClient.forUrl(getTypeLookup(), () -> target); + LiveEnvModel env = client.getEnv(); + if (env != null) { + return Failable.of(env); + } + } + return Failable.error(getBootDashModel().getRunTarget().getType().getMissingLiveInfoMessages().getMissingInfoMessage(getName(), "env")); + } + + }; + liveEnv.dependsOn(actuatorUrl); + addElementState(liveEnv); + addDisposableChild(liveEnv); + } + } + return liveEnv.getValue(); + } + + protected LiveExpression getActuatorUrl() { + return LiveExpression.constant(null); + } + + @Override + public CloudFoundryRunTarget getTarget() { + return (CloudFoundryRunTarget) super.getTarget(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudDashElementFactory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudDashElementFactory.java new file mode 100644 index 000000000..2684a765e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudDashElementFactory.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.model; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFServiceInstance; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModelContext; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; + +public class CloudDashElementFactory { + + private final IPropertyStore modelStore; + + private final CloudFoundryBootDashModel model; + + public CloudDashElementFactory(BootDashModelContext context, IPropertyStore modelStore, + CloudFoundryBootDashModel model) { + this.modelStore = modelStore; + this.model = model; + } + + public CloudServiceInstanceDashElement createService(CFServiceInstance service) { + return new CloudServiceInstanceDashElement(model, service, modelStore); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudFoundryBootDashModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudFoundryBootDashModel.java new file mode 100644 index 000000000..512080131 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudFoundryBootDashModel.java @@ -0,0 +1,868 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.cf.model; + +import org.springsource.ide.eclipse.commons.livexp.core.LiveSets; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +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.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.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.OperationCanceledException; +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.api.Deletable; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplicationDetail; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.ReactorUtils; +import org.springframework.ide.eclipse.boot.dash.cf.debug.DebugStrategyManager; +import org.springframework.ide.eclipse.boot.dash.cf.debug.DebugSupport; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.ApplicationManifestHandler; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudData; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.UnsupportedPushProperties; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.YamlGraphDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialog; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialogModel.ManifestType; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.StoreCredentialsMode; +import org.springframework.ide.eclipse.boot.dash.cf.ops.JobBody; +import org.springframework.ide.eclipse.boot.dash.cf.ops.Operation; +import org.springframework.ide.eclipse.boot.dash.cf.ops.OperationsExecution; +import org.springframework.ide.eclipse.boot.dash.cf.ops.ProjectsDeployer; +import org.springframework.ide.eclipse.boot.dash.cf.ops.ServicesRefreshOperation; +import org.springframework.ide.eclipse.boot.dash.cf.ops.TargetApplicationsRefreshOperation; +import org.springframework.ide.eclipse.boot.dash.cf.packaging.CloudApplicationArchiverStrategies; +import org.springframework.ide.eclipse.boot.dash.cf.packaging.CloudApplicationArchiverStrategy; +import org.springframework.ide.eclipse.boot.dash.cf.packaging.ICloudApplicationArchiver; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.cf.ui.CfUserInteractions; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.RemoteBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.YamlFileInput; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.YamlInput; +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.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.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.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.AsyncMode; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; +import org.springsource.ide.eclipse.commons.livexp.util.OldValueDisposer; +import org.yaml.snakeyaml.Yaml; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class CloudFoundryBootDashModel extends RemoteBootDashModel { + + private DebugStrategyManager cfDebugStrategies; + + public static final String APP_TO_PROJECT_MAPPING = "projectToAppMapping"; + + private static final Comparator ELEMENT_COMPARATOR = new Comparator() { + @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 services = new LiveSetVariable<>(AsyncMode.SYNC); + private final CloudDashApplications applications = new CloudDashApplications(this); + private final ObservableSet allElements = LiveSets.union(applications.getApplications(), services); + + private void checkApiVersion() { + this.refreshTracker.callAsync("Checking API version...", () -> { + ClientRequests client = getClient(); + if (client!=null) { + Version server = client.getApiVersion(); + Version supported = client.getSupportedApiVersion(); + if (server.compareTo(supported)<0) { + throw ExceptionUtil.coreException(IStatus.WARNING, "Client supports API version "+server+ + " and is connected to server with API version "+supported+". "+ + "Things may not work as expected."); + } + } + return null; + }); + } + + 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 renamedFrom = new HashMap<>(); + Map renamedTo = new HashMap<>(); + List 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 appsToRefresh = new ArrayList<>(); + for (IProject project : removedProjects) { + CloudAppDashElement app = getApplication(project); + if (app!=null && app.setProject(null)) { + appsToRefresh.add(app); + } + } + for (Map.Entry 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 (Exception e) { + Log.log(e); + } + } + }; + + @Override + public RefreshState getRefreshState() { + return refreshTracker.refreshState.getValue(); + } + + private OldValueDisposer refreshTokenDisposer = new OldValueDisposer<>(this); + + /** + * This tracks the number of activeRefreshTokenListeners. It is used for debugging and testing purposes only. (To + * observe whether the listeners are properly disposed). + */ + public final AtomicInteger activeRefreshTokenListeners = new AtomicInteger(); + + public CloudFoundryBootDashModel(CloudFoundryRunTarget target, BootDashModelContext context, BootDashViewModel parent) { + super(target, parent); + cfDebugStrategies = new DebugStrategyManager(injections().getBeans(DebugSupport.class), getViewModel()); + this.elementFactory = new CloudDashElementFactory(context, target.getPropertyStore(), this); + + this.unsupportedPushProperties = new UnsupportedPushProperties(); + addDisposableChild(target.getClientExp().onChange((exp,v) -> { + this.refresh(ui()); + ClientRequests client = exp.getValue(); + if (client!=null && this.getRunTarget().getTargetProperties().getStoreCredentials()==StoreCredentialsMode.STORE_TOKEN) { + activeRefreshTokenListeners.incrementAndGet(); + refreshTokenDisposer.setValue(client.getRefreshTokens().doOnNext(refreshToken -> { + try { + this.getRunTarget().getTargetProperties().setCredentials(CFCredentials.fromRefreshToken(refreshToken)); + } catch (CannotAccessPropertyException e) { + Log.log(e); + } + }) + .doOnTerminate(activeRefreshTokenListeners::decrementAndGet) + .subscribe()); + } + checkApiVersion(); + })); + 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 +// Log.async(this.connect(ConnectMode.AUTOMATIC)); +// } +// } catch (CannotAccessPropertyException e1) { +// // TODO Auto-generated catch block +// e1.printStackTrace(); +// } + } + + @Override + public ObservableSet getElements() { + return allElements; + } + + @Override + public void dispose() { + if (cfDebugStrategies!=null) { + cfDebugStrategies.dispose(); + } + 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 getElementComparator() { + return ELEMENT_COMPARATOR; + } + + @Override + public CloudFoundryRunTarget getRunTarget() { + return (CloudFoundryRunTarget) super.getRunTarget(); + } + + @Override + public boolean canBeAdded(List 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; + } + + 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; + } + + @Override + public void performDeployment( + final Set projectsToDeploy, + RunState runOrDebug + ) throws Exception { + DebugSupport debugSuppport = getDebugSupport(); + 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; + } + + @Override + public CloudAppDashElement getApplication(String appName) { + Set apps = getApplications().getValues(); + for (CloudAppDashElement element : apps) { + if (appName.equals(element.getName())) { + return element; + } + } + return null; + } + + public CloudServiceInstanceDashElement getService(String serviceName) { + ImmutableSet services = getServices().getValues(); + for (CloudServiceInstanceDashElement s : services) { + if (s.getName().equals(serviceName)) { + return s; + } + } + return null; + } + + private CloudAppDashElement getApplication(IProject project) { + Set 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 names) { + applications.setAppNames(names); + } + + public void updateElements(Collection 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.of()); + } else { + synchronized (this) { + applications.setAppNames(getNames(apps)); + for (CFApplicationDetail appDetails : apps) { + CloudAppDashElement app = applications.getApplication(appDetails.getName()); + app.setDetailedData(appDetails); + } + } + } + } + + private ImmutableList getNames(Collection apps) { + ImmutableList.Builder 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 toRemove, UserInteractions ui) { + if (toRemove == null || toRemove.isEmpty()) { + return; + } + List> 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(); + } 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 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() + ")"; + } + + 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 = cfUi().openManifestDiffDialog(model); + if (result==null) { + result = Result.USE_MANIFEST; + } + 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 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, true); + IFile foundManifestFile = DeploymentPropertiesDialog.findManifestYamlFile(project); + dialogModel.setSelectedManifest(foundManifestFile); + dialogModel.setManifestType(foundManifestFile == null ? ManifestType.MANUAL : ManifestType.FILE); + + props = cfUi().promptApplicationDeploymentProperties(dialogModel); + + addApplicationArchive(project, props, cloudData, ui, monitor); + } + return props; + } + + private CfUserInteractions cfUi() { + return injections().getBean(CfUserInteractions.class); + } + + 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 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 newServices) { + this.services.replaceAll(newServices); + } + + public ObservableSet getApplications() { + return applications.getApplications(); + } + + public ImmutableSet getApplicationValues() { + return applications.getApplicationValues(); + } + + public ObservableSet 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 ClientRequests getClient() { + return getRunTarget().getClient(); + } + + public List 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 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 UnsupportedPushProperties getUnsupportedProperties() { + return unsupportedPushProperties; + } + + public DebugSupport getDebugSupport() { + return cfDebugStrategies.getStrategy(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudServiceInstanceDashElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudServiceInstanceDashElement.java new file mode 100644 index 000000000..2b8927858 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/model/CloudServiceInstanceDashElement.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * Copyright (c) 2016, 2020 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.cf.model; + +import java.util.EnumSet; + +import org.eclipse.core.resources.IProject; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFServiceInstance; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +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.RunState; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; + +import reactor.core.publisher.Mono; + +public class CloudServiceInstanceDashElement extends CloudDashElement implements AsyncDeletable { + + private static final EnumSet SERVICE_RUN_GOAL_STATES = EnumSet.noneOf(RunState.class); + + private static final BootDashColumn[] COLUMNS = {BootDashColumn.NAME, BootDashColumn.TAGS}; + + private final CFServiceInstance service; + private final PropertyStoreApi persistentProperties; + + public CloudServiceInstanceDashElement(AbstractBootDashModel parent, CFServiceInstance service, IPropertyStore modelStore) { + super(parent, service.getName()+"@"+parent.getRunTarget().getId()); + this.service = service; + IPropertyStore backingStore = PropertyStores.createSubStore("S"+getName(), modelStore); + this.persistentProperties = PropertyStores.createApi(backingStore); + } + + @Override + public CloudFoundryBootDashModel getBootDashModel() { + return (CloudFoundryBootDashModel) super.getBootDashModel(); + } + + @Override + public Object getParent() { + return getBootDashModel(); + } + + @Override + public int getLivePort() { + return -1; + } + + @Override + public String getLiveHost() { + return null; + } + + @Override + public ILaunchConfiguration getActiveConfig() { + return null; + } + + @Override + public void stop() throws Exception { + } + + @Override + public void restart(RunState runingOrDebugging, UserInteractions ui) throws Exception { + } + + @Override + public void openConfig(UserInteractions ui) { + } + + @Override + public int getActualInstances() { + return 0; + } + + @Override + public int getDesiredInstances() { + return 0; + } + + @Override + public String getName() { + return service.getName(); + } + + @Override + public BootDashColumn[] getColumns() { + return COLUMNS; + } + + @Override + public PropertyStoreApi getPersistentProperties() { + return persistentProperties; + } + + @Override + public IProject getProject() { + return null; + } + + @Override + public RunState getRunState() { + return null; + } + + @Override + public String getUrl() { + return service != null ? service.getDashboardUrl() : null; + } + + public String getDocumentationUrl() { + return service!=null ? service.getDocumentationUrl() : null; + } + + public CFServiceInstance getCloudService() { + return service; + } + + public String getPlan() { + CFServiceInstance s = getCloudService(); + return s==null?null:s.getPlan(); + } + + public String getService() { + CFServiceInstance s = getCloudService(); + return s==null?null:s.getService(); + } + + public String getDescription() { + CFServiceInstance s = getCloudService(); + return s==null?null:s.getDescription(); + } + + @Override + public Mono deleteAsync() { + CloudFoundryBootDashModel model = getBootDashModel(); + String serviceName = getName(); + return Mono.fromRunnable(this::cancelOperations) + .then(getClient().deleteServiceAsync(serviceName)) + .doOnSuccess((ignore) -> model.removeService(serviceName)); + } + + private ClientRequests getClient() { + return getTarget().getClient(); + } + + @Override + public EnumSet supportedGoalStates() { + return SERVICE_RUN_GOAL_STATES; + } + + @Override + public ImageDescriptor getCustomRunStateIcon() { + return BootDashActivator.getDefault().getImageRegistry().getDescriptor(BootDashActivator.SERVICE_ICON); + } + + @Override + public Image getPropertiesTitleIconImage() { + return BootDashActivator.getDefault().getImageRegistry().get(BootDashActivator.SERVICE_ICON); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/AppInstancesRefreshOperation.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/AppInstancesRefreshOperation.java new file mode 100644 index 000000000..9b550c3c3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/AppInstancesRefreshOperation.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.ops; + +import java.time.Duration; +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplicationDetail; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.RefreshState; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; + +import reactor.core.publisher.Flux; + +/** + * Refreshes the application instances. + *

+ * This will indirectly refresh the application running state as the running + * state of an app is resolved from the number of running instances + * + */ +public class AppInstancesRefreshOperation extends CloudOperation { + + private List appsToLookUp; + + public AppInstancesRefreshOperation(CloudFoundryBootDashModel model, List appsToLookUp) { + super("Refreshing running state of applications in: " + model.getRunTarget().getName(), model); + this.appsToLookUp = appsToLookUp; + } + + @Override + protected void doCloudOp(IProgressMonitor monitor) throws Exception { + this.model.refreshTracker.call("Fetching App Instances...", () -> { + if (!appsToLookUp.isEmpty()) { + Duration timeToWait = Duration.ofSeconds(30); + ClientRequests client = model.getRunTarget().getClient(); + if (client!=null) { + client.getApplicationDetails(appsToLookUp) + .doOnNext(this.model::updateApplication) + .then() + .block(timeToWait); + } + } + return null; + }); + } + + @Override + public ISchedulingRule getSchedulingRule() { + return new RefreshSchedulingRule(model.getRunTarget()); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/ApplicationOperationEvent.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/ApplicationOperationEvent.java new file mode 100644 index 000000000..c5f428d49 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/ApplicationOperationEvent.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.ops; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CloudAppInstances; + +/** + * Event that occurs during application operations. + */ +public interface ApplicationOperationEvent { + + void fire(); + + CloudAppInstances getAppInstances(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/CloudApplicationOperation.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/CloudApplicationOperation.java new file mode 100644 index 000000000..3d646ceeb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/CloudApplicationOperation.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2015, 2018 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.cf.ops; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.console.LogType; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; + +/** + * A cloud operation that is performed on a Cloud application (for example, + * creating, starting, or stopping an application) + */ +public abstract class CloudApplicationOperation extends CloudOperation { + + protected String appName; + private ISchedulingRule schedulingRule; + private final CancelationToken cancelationToken; + + public CloudApplicationOperation(String opName, CloudFoundryBootDashModel model, String appName, CancelationToken cancelationToken) { + super(opName, model); + this.cancelationToken = cancelationToken; + this.appName = appName; + setSchedulingRule(new StartApplicationSchedulingRule(model.getRunTarget(), appName)); + } + + protected CloudAppDashElement getDashElement() { + return model.getApplication(appName); + } + + @Override + public ISchedulingRule getSchedulingRule() { + return this.schedulingRule; + } + + public void setSchedulingRule(ISchedulingRule schedulingRule) { + this.schedulingRule = schedulingRule; + } + + protected void log(String message) { + try { + model.getElementConsoleManager().writeToConsole(getDashElement(), message, LogType.STDOUT); + } catch (Exception e) { + BootDashActivator.log(e); + } + } + + protected void logAndUpdateMonitor(String message, IProgressMonitor monitor) { + if (monitor != null) { + monitor.setTaskName(message); + } + try { + model.getElementConsoleManager().writeToConsole(getDashElement(), message, LogType.STDOUT); + } catch (Exception e) { + BootDashActivator.log(e); + } + } + + @Override + String getOpErrorPrefix() { + return "Error: " + appName + " in '" + model.getRunTarget().getName() + "'"; + } + + public void checkTerminationRequested(IProgressMonitor mon) throws OperationCanceledException { + if ( + mon!=null && mon.isCanceled() || + cancelationToken!=null && cancelationToken.isCanceled() + ) { + throw new OperationCanceledException(); + } + } + + protected CancelationToken getCancelationToken() { + return cancelationToken; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/CloudOperation.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/CloudOperation.java new file mode 100644 index 000000000..c1e324957 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/CloudOperation.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.ops; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.cf.ui.CfUserInteractions; +import org.springframework.ide.eclipse.boot.dash.cf.util.CloudErrors; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +public abstract class CloudOperation extends Operation { + + protected final CloudFoundryBootDashModel model; + + public CloudOperation(String opName, CloudFoundryBootDashModel model) { + super(opName); + this.model = model; + } + + abstract protected void doCloudOp(IProgressMonitor monitor) throws Exception, OperationCanceledException; + + @Override + protected Void runOp(IProgressMonitor monitor) throws Exception, OperationCanceledException { + if (monitor.isCanceled()) { + throw new OperationCanceledException(); + } + try { + doCloudOp(monitor); + } catch (Exception e) { + CloudErrors.checkAndRethrowCloudException(e, getOpErrorPrefix()); + } + return null; + } + + String getOpErrorPrefix() { + return "Error in target: " + model.getRunTarget().getName(); + } + + protected CloudFoundryRunTarget getRunTarget() { + return model.getRunTarget(); + } + + protected ClientRequests getClientRequests() { + CloudFoundryRunTarget target = getRunTarget(); + if (target.isConnected()) { + return target.getClient(); + } + return null; + } + + protected SimpleDIContext getDIContext() { + return model.getViewModel().getContext().injections; + } + + protected CfUserInteractions cfUi() { + return getDIContext().getBean(CfUserInteractions.class); + } + + protected UserInteractions ui() { + return getDIContext().getBean(UserInteractions.class); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/CloudSchedulingRule.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/CloudSchedulingRule.java new file mode 100644 index 000000000..7eae5f75c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/CloudSchedulingRule.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.ops; + +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; + +public class CloudSchedulingRule implements ISchedulingRule { + + protected final RunTarget runTarget; + + public CloudSchedulingRule(RunTarget runTarget) { + this.runTarget = runTarget; + } + + @Override + public boolean contains(ISchedulingRule rule) { + return rule == this; + } + + @Override + public boolean isConflicting(ISchedulingRule rule) { + if (rule instanceof CloudSchedulingRule) { + CloudSchedulingRule otherRefreshRule = (CloudSchedulingRule) rule; + if (otherRefreshRule.getRunTarget().getId().equals(this.getRunTarget().getId())) { + return true; + } + } + return false; + } + + public RunTarget getRunTarget() { + return runTarget; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/JobBody.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/JobBody.java new file mode 100644 index 000000000..e8331b138 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/JobBody.java @@ -0,0 +1,17 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.ops; + +import org.eclipse.core.runtime.IProgressMonitor; + +public interface JobBody { + public void run(IProgressMonitor mon) throws Exception; +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/Operation.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/Operation.java new file mode 100644 index 000000000..4ed3d2c1f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/Operation.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.ops; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.eclipse.jface.operation.IRunnableContext; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.springframework.ide.eclipse.boot.dash.model.Nameable; + +public abstract class Operation implements Nameable { + + protected final String opName; + + public Operation(String opName) { + this.opName = opName; + } + + public String getName() { + return this.opName; + } + + public T run(IProgressMonitor monitor) throws Exception, OperationCanceledException { + monitor.setTaskName(getName()); + return runOp(monitor); + } + + protected abstract T runOp(IProgressMonitor monitor) throws Exception, OperationCanceledException; + + /** + * Optional. Return null if no scheduling rule is needed + * + * @return + */ + public ISchedulingRule getSchedulingRule() { + return null; + } + + public T run(IRunnableContext context, boolean fork) throws Exception { + + final List val = new ArrayList(); + IRunnableWithProgress runnable = new IRunnableWithProgress() { + + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + try { + val.add(Operation.this.run(monitor)); + } catch (Exception e) { + throw new InvocationTargetException(e); + } + } + }; + try { + context.run(fork, true, runnable); + + } catch (InvocationTargetException ite) { + // Don't throw the wrapper + throw ite.getTargetException() instanceof Exception ? (Exception) ite.getTargetException() : ite; + } + return val.get(0); + } + + @Override + public String toString() { + return getName(); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/OperationsExecution.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/OperationsExecution.java new file mode 100644 index 000000000..54fcf3bfb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/OperationsExecution.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.ops; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.eclipse.core.runtime.jobs.Job; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +public class OperationsExecution { + + + private final CloudFoundryBootDashModel model; + + public OperationsExecution(CloudFoundryBootDashModel model) { + this.model = model; + } + + public void runAsynch(String opName, String appName, JobBody runnable, UserInteractions ui) { + runAsynch(new CloudApplicationOperation(opName, model, appName, CancelationTokens.NULL) { + @Override + protected void doCloudOp(IProgressMonitor monitor) throws Exception, OperationCanceledException { + runnable.run(monitor); + } + }, ui); + } + + public void runAsynch(final Operation op) { + runAsynch(op, null); + } + + public void runAsynch(final Operation op, UserInteractions ui) { + if (op!=null) { + Job job = new Job(op.getName()) { + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + op.run(monitor); + } catch (Exception e) { + if (!ExceptionUtil.isCancelation(e)) { + if (ui != null) { + String message = e.getMessage() != null && e.getMessage().trim().length() > 0 + ? e.getMessage() + : "Error type: " + e.getClass().getName() + + ". Check Error Log view for further details."; + ui.errorPopup("Operation Failure", message); + + } + Log.log(e); + } + } + // Only return OK status to avoid a second error dialogue + // appearing, which is opened by Eclipse when a job returns + // error status. + return Status.OK_STATUS; + } + + }; + + ISchedulingRule rule = op.getSchedulingRule(); + if (rule != null) { + job.setRule(rule); + } + + job.setPriority(Job.INTERACTIVE); + job.schedule(); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/ProjectsDeployer.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/ProjectsDeployer.java new file mode 100644 index 000000000..2458a8ed4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/ProjectsDeployer.java @@ -0,0 +1,248 @@ +/******************************************************************************* + * Copyright (c) 2015, 2018 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.cf.ops; + +import java.io.StringWriter; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Consumer; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplicationDetail; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFPushArguments; +import org.springframework.ide.eclipse.boot.dash.cf.debug.DebugSupport; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudData; +import org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.JmxSupport; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.dialogs.ManifestDiffDialogModel.Result; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.LocalRunTarget; +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.util.CancelationTokens; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +public class ProjectsDeployer extends CloudOperation { + + private static final boolean DEBUG = + (""+Platform.getLocation()).contains("bamboo") || + (""+Platform.getLocation()).contains("kdvolder"); + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + private final Set projectsToDeploy; + private final UserInteractions ui; + private final RunState runOrDebug; + private final DebugSupport debugSupport; + + public ProjectsDeployer(CloudFoundryBootDashModel model, + UserInteractions ui, + Set projectsToDeploy, + RunState runOrDebug, + DebugSupport debugSupport) { + super("Deploying projects", model); + this.projectsToDeploy = projectsToDeploy; + this.ui = ui; + this.runOrDebug = runOrDebug; + this.debugSupport = debugSupport; + } + + @Override + protected void doCloudOp(IProgressMonitor monitor) throws Exception, OperationCanceledException { + monitor.beginTask("Deploy projects", projectsToDeploy.size()); + try { + for (Iterator it = projectsToDeploy.iterator(); it.hasNext();) { + IProject project = it.next(); + if (monitor.isCanceled()) { + throw new OperationCanceledException(); + } + deployProject(project, new SubProgressMonitor(monitor, 1)); + } + } finally { + monitor.done(); + } + } + + private void deployProject(IProject project, IProgressMonitor monitor) throws Exception { + debug("deployProject["+project.getName()+"] starting"); + CloudApplicationDeploymentProperties properties = model.createDeploymentProperties(project, ui, monitor); + debug("deployProject["+project.getName()+"] got deployment properties"); + + // IMPORTANT: We check for unsupported properties BEFORE creating the CDE, so that if operation is cancelled, + // the cde has not been created and it avoids having it appear in the boot dash view + model.getUnsupportedProperties().allowOrCancelIfFound(ui, properties); + + CloudAppDashElement cde = model.ensureApplication(properties.getAppName()); + debug("deployProject["+project.getName()+"] created cde: "+cde.getName()); + model.runAsynch("Deploy project '"+project.getName()+"'", properties.getAppName(), (IProgressMonitor progressMonitor) -> { + doDeployProject(cde, properties, project, progressMonitor); + }, ui); + } + + protected CloudData buildOperationCloudData(IProgressMonitor monitor, IProject project) throws Exception { + return new CloudData(getRunTarget().getDomains(monitor), getRunTarget().getBuildpack(project), getRunTarget().getStacks(monitor)); + } + + protected void doDeployProject(CloudAppDashElement cde, CloudApplicationDeploymentProperties initialProperties, + IProject project, IProgressMonitor monitor) throws Exception { + ClientRequests client = model.getRunTarget().getClient(); + CancelationToken cancelationToken = cde.createCancelationToken(); + + IFile manifestFile = initialProperties.getManifestFile(); + + // An app may already exist in CF, so when confirming to replace the exist app below, the option to + // use the existing app deployment properties rather than the initial properties may result in a different deployment + // properties. Therefore use a LiveExpression to support possible change in the properties to be used. + LiveVariable pushPropertiesToUse = new LiveVariable<>( + initialProperties); + pushPropertiesToUse.addListener((exp, val) -> { + // Make sure that the local archive gets set in the properties, even on changes, + // because + // we want to push the locally archived sources regardless of which other + // properties have changed (e.g. bound services, env vars, etc...) + val.setArchive(initialProperties.getArchive()); + val.setEnableJmxSshTunnel(initialProperties.getEnableJmxSshTunnel()); + + }); + + try { + + cde.whileStarting(ui, cancelationToken, monitor, () -> { + CFApplicationDetail existingCloudApp = client.getApplication(initialProperties.getAppName()); + if (existingCloudApp != null) { + CloudData cloudData = buildOperationCloudData(monitor, project); + CloudApplicationDeploymentProperties existingAppProperties = CloudApplicationDeploymentProperties.getFor(project, cloudData, existingCloudApp); + + confirmReplaceApp(cloudData, cde, existingAppProperties, project, manifestFile, monitor, (result) -> { + switch (result) { + case CANCELED: + cde.print("Canceled pushing project '"+project.getName() + "'"); + throw new OperationCanceledException(); + case USE_MANIFEST: + // Initial properties were generated from Manifest some point earlier so just set the initial properties + // if using manifest + pushPropertiesToUse.setValue(initialProperties); + cde.setDeploymentManifestFile(initialProperties.getManifestFile()); + break; + case FORGET_MANIFEST: + // "Forget Manifest" means use the existing Cloud app deployment properties (e.g retain existing bound services, memory, env vars, etc..). Only the + // app content will be replaced + cde.setDeploymentManifestFile(null); + pushPropertiesToUse.setValue(existingAppProperties); + break; + } + }); + } else { + cde.setDeploymentManifestFile(manifestFile); + } + cde.setProject(project); + copyTags(project, cde); + cde.print("Pushing project '"+project.getName()+"'"); + cde.setEnableJmxSshTunnel(pushPropertiesToUse.getValue().getEnableJmxSshTunnel()); + JmxSupport jmxSupport = cde.getJmxSupport(); + try (CFPushArguments args = pushPropertiesToUse.getValue().toPushArguments(model.getCloudDomains(monitor))) { + if (jmxSupport!=null) jmxSupport.setupEnvVars(args.getEnv()); + if (isDebugEnabled()) { + debugSupport.setupEnvVars(args.getEnv()); + } + client.push(args, CancelationTokens.merge(cancelationToken, monitor)); + cde.print("Pushing project '"+project.getName()+"' SUCCEEDED!"); + pushPropertiesToUse.close(); + } + if (cde.refresh()!=null) { + //Careful... connecting the debugger must be done after the refresh because it needs the app guid which + // won't be available for a newly created element if its not yet been populated with data from CF. + if (isDebugEnabled()) { + debugSupport.createOperation(cde, "Connect Debugger for "+cde.getName() , ui, cancelationToken).runOp(monitor); + } + } + }); + } catch (Exception e) { + cde.refresh(); + cde.printError("Pushing FAILED!"); + if (!ExceptionUtil.isCancelation(e)) { + Log.log(e); + if (ui != null) { + String message = e.getMessage() != null && e.getMessage().trim().length() > 0 ? e.getMessage() + : "Error type: " + e.getClass().getName() + + ". Check Error Log view for further details."; + ui.errorPopup("Operation Failure", message); + } + } + } + } + + private boolean isDebugEnabled() { + return runOrDebug==RunState.DEBUGGING && debugSupport!=null; + } + + private void copyTags(IProject project, CloudAppDashElement cde) { + BootDashElement localElement = findLocalBdeForProject(project); + if (localElement!=null) { + copyTags(localElement, cde); + } + } + + private BootDashElement findLocalBdeForProject(IProject project) { + BootDashModel localModel = model.getViewModel().getSectionByTargetId(LocalRunTarget.INSTANCE.getId()); + if (localModel != null) { + for (BootDashElement bde : localModel.getElements().getValue()) { + if (project.equals(bde.getProject())) { + return bde; + } + } + } + return null; + } + + private static void copyTags(BootDashElement sourceBde, BootDashElement targetBde) { + LinkedHashSet tagsToCopy = sourceBde.getTags(); + if (tagsToCopy != null && !tagsToCopy.isEmpty()) { + LinkedHashSet targetTags = targetBde.getTags(); + for (String tag : tagsToCopy) { + targetTags.add(tag); + } + targetBde.setTags(targetTags); + } + } + + private void confirmReplaceApp(CloudData cloudData, CloudAppDashElement element, CloudApplicationDeploymentProperties existingAppProperties, IProject project, IFile manifestFile, IProgressMonitor monitor, Consumer handleResult) throws Exception{ + StringWriter writer = new StringWriter(); + writer.append("Replace existing Cloud application - "); + writer.append(existingAppProperties.getAppName()); + writer.append(" - with the project '"); + writer.append(project.getName()); + writer.append("'?"); + + String title = writer.toString(); + + Result result = cfUi().confirmReplaceApp(title, cloudData, manifestFile, existingAppProperties); + + handleResult.accept(result); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/RefreshSchedulingRule.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/RefreshSchedulingRule.java new file mode 100644 index 000000000..102b1b1ab --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/RefreshSchedulingRule.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.ops; + +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; + +public class RefreshSchedulingRule extends CloudSchedulingRule { + + public RefreshSchedulingRule(RunTarget runTarget) { + super(runTarget); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/RemoteDevClientStartOperation.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/RemoteDevClientStartOperation.java new file mode 100644 index 000000000..b497bb94a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/RemoteDevClientStartOperation.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.ops; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.Status; +import org.eclipse.debug.core.ILaunchManager; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsUtil; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; + +/** + * Operation for (re)starting Remote DevTools Client + * + * @author Alex Boyko + * + */ +public class RemoteDevClientStartOperation extends CloudApplicationOperation { + + private final RunState startMode; + + public RemoteDevClientStartOperation(CloudFoundryBootDashModel model, String appName, RunState startMode, CancelationToken cancelationToken) { + super("Starting Remote DevTools Client for application '" + appName + "'", model, appName, cancelationToken); + this.startMode = startMode; + } + + @Override + protected void doCloudOp(IProgressMonitor monitor) throws Exception, OperationCanceledException { + CloudAppDashElement cde = model.getApplication(appName); + if (cde == null || cde.getProject() == null) { + throw new CoreException(new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, "Local project not associated to CF app '" + appName + "'")); + } + DevtoolsUtil.disconnectDevtoolsClientsFor(cde); + //TODO: create a 'monitor' that is aware of cancelation token to apss to the launch operation. + DevtoolsUtil.launchDevtools(cde, DevtoolsUtil.getSecret(cde.getProject()), startMode == RunState.DEBUGGING ? ILaunchManager.DEBUG_MODE : ILaunchManager.RUN_MODE , monitor); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/SelectManifestOp.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/SelectManifestOp.java new file mode 100644 index 000000000..7c5a59acd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/SelectManifestOp.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) 2016, 2020 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.cf.ops; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudData; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialogModel.ManifestType; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +public class SelectManifestOp extends CloudOperation { + + protected final CloudAppDashElement cde; + + public SelectManifestOp(CloudAppDashElement cde) { + super("Select a manifest file", cde.getBootDashModel()); + this.cde = cde; + } + + @Override + protected void doCloudOp(final IProgressMonitor monitor) throws Exception, OperationCanceledException { + + IProject project = cde.getProject(); + + if (cde == null || project == null) { + return; + } + + IFile manifest = cde.getDeploymentManifestFile(); + + /* + * Commented out because manual manifest contents based on current + * deployment props from CF don't need to be the latest since they are + * not editable + */ + // /* +// * Refresh the latest cloud application +// */ +// new RefreshApplications(model, Collections.singletonList(model.getAppCache().getApp(project))).run(monitor); + + CloudData cloudData = model.buildOperationCloudData(monitor, project); + + DeploymentPropertiesDialogModel dialogModel = new DeploymentPropertiesDialogModel(ui(), cloudData, project, cde.getSummaryData(), false); + dialogModel.setSelectedManifest(manifest); + dialogModel.setManifestType(manifest == null ? ManifestType.MANUAL : ManifestType.FILE); + CloudApplicationDeploymentProperties props = cfUi().promptApplicationDeploymentProperties(dialogModel); + + if (props == null) { + throw ExceptionUtil.coreException("Error loading deployment properties from the manifest YAML"); + } + + cde.setDeploymentManifestFile(props.getManifestFile()); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/ServicesRefreshOperation.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/ServicesRefreshOperation.java new file mode 100644 index 000000000..11f10c3cb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/ServicesRefreshOperation.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.ops; + +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFServiceInstance; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudDashElementFactory; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudServiceInstanceDashElement; +import org.springframework.ide.eclipse.boot.util.Log; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; + +public class ServicesRefreshOperation extends CloudOperation{ + +// private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder"); + +// private static void debug(String msg) { +// if (DEBUG) { +// System.out.println(msg); +// } +// } + + final private CloudDashElementFactory elementFactory; + + public ServicesRefreshOperation(CloudFoundryBootDashModel model) { + super("Refresh Cloud Services", model); + this.elementFactory = model.getElementFactory(); + } + + @Override + protected void doCloudOp(IProgressMonitor monitor) throws Exception, OperationCanceledException { + monitor.beginTask("Refresh services", 2); + boolean success = false; + try { + ClientRequests client = getClientRequests(); + monitor.worked(1); + if (client!=null) { +// debug("Resfres Services for connected client"); + List serviceInfos = client.getServices(); + Builder services = ImmutableSet.builder(); + for (CFServiceInstance service : serviceInfos) { + services.add(elementFactory.createService(service)); + } + model.setServices(services.build()); + success = true; + } + } catch (Exception e) { + //If Network is down, typically the same error will happen in parallel for refresing the aps. + //We don't want a double popup so just log this here instead of letting it fly. + //Note: handling this would be possible if the operations where able to parallel composed so there can + //be a single handler attached that is called when both of them are complete. + Log.log(e); + } finally { + if (!success) { +// debug("Resfresh Services for DISconnected client"); + model.setServices(ImmutableSet.of()); + } + monitor.done(); + } + } + + public ISchedulingRule getSchedulingRule() { + return new RefreshSchedulingRule(model.getRunTarget()); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/SetHealthCheckOperation.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/SetHealthCheckOperation.java new file mode 100644 index 000000000..36458ae8e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/SetHealthCheckOperation.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.ops; + +import java.util.UUID; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; + +/** + * @author Kris De Volder + */ +public class SetHealthCheckOperation extends CloudApplicationOperation { + + private String hcType; + private CloudAppDashElement app; + private UserInteractions ui; + private boolean confirmChange; + private static final String CONFIRM_CHANGE_KEY = SetHealthCheckOperation.class.getName()+".confirm"; + + public SetHealthCheckOperation(CloudAppDashElement app, String hcType, UserInteractions ui, boolean confirmChange, CancelationToken cancelationToken) { + super("set-health-check "+app.getName()+" "+hcType, app.getBootDashModel(), app.getName(), cancelationToken); + this.app = app; + this.hcType = hcType; + this.ui = ui; + this.confirmChange = confirmChange; + } + + public SetHealthCheckOperation(CloudAppDashElement app, String hcType, CancelationToken cancelationToken) { + this(app, hcType, null, false, cancelationToken); + } + + @Override + protected void doCloudOp(IProgressMonitor monitor) throws Exception, OperationCanceledException { + monitor.beginTask(getName(), 2); + try { + ClientRequests client = getClientRequests(); + UUID guid = app.getAppGuid(); + String current = client.getHealthCheck(guid); + //When current==null it means that there's no 'health-check' info in the info returned by + //cloudcontroller. Probably this means app has no support for this and so we shouldn't try to + //set it. + monitor.worked(1); + if (current!=null) { + if (!current.equals(hcType)) { + boolean confirmed = true; + if (confirmChange) { + confirmed = ui.yesNoWithToggle(CONFIRM_CHANGE_KEY, "Set health check for '"+app+"' to '"+hcType+"'?", + "Health check is currently set to '"+current+"'. This may interfere with Spring Boot Devtools "+ + "reload functionality. Do you want to set it to '"+hcType+"'?", + "Remember my decision" + ); + } + if (confirmed) { + client.setHealthCheck(guid, hcType); + app.setHealthCheck(hcType); + } + monitor.worked(1); + } + } + } finally { + monitor.done(); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/StartApplicationSchedulingRule.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/StartApplicationSchedulingRule.java new file mode 100644 index 000000000..e4c5da174 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/StartApplicationSchedulingRule.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.ops; + +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; + +public class StartApplicationSchedulingRule extends CloudSchedulingRule { + + private final String appName; + + public StartApplicationSchedulingRule(RunTarget runTarget, String appName) { + super(runTarget); + this.appName = appName; + } + + @Override + public boolean isConflicting(ISchedulingRule otherRule) { + if (otherRule instanceof CloudSchedulingRule) { + CloudSchedulingRule otherCloudRule = (CloudSchedulingRule) otherRule; + + // Possibly in conflict only if the other operation is also on the + // same run target + if (otherCloudRule.getRunTarget().getId().equals(this.getRunTarget().getId())) { + // Application ops for the same target cannot run in parallel + // with that target's refresh op + if (otherCloudRule instanceof RefreshSchedulingRule) { + return true; + } else if (otherCloudRule instanceof StartApplicationSchedulingRule) { + + StartApplicationSchedulingRule otherApplicationSchedlingRule = (StartApplicationSchedulingRule) otherCloudRule; + + // Only operation per app can be running. Other app + // operations can run in parallel for other apps + return otherApplicationSchedlingRule.getAppName().equals(this.getAppName()); + } + + } + } + return false; + } + + public String getAppName() { + return appName; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/StopApplicationSchedulingRule.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/StopApplicationSchedulingRule.java new file mode 100644 index 000000000..37306a778 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/StopApplicationSchedulingRule.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.ops; + +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; + +public class StopApplicationSchedulingRule extends CloudSchedulingRule { + + private final String appName; + + public StopApplicationSchedulingRule(RunTarget runTarget, String appName) { + super(runTarget); + this.appName = appName; + } + + @Override + public boolean isConflicting(ISchedulingRule otherRule) { + if (otherRule instanceof CloudSchedulingRule) { + CloudSchedulingRule otherCloudRule = (CloudSchedulingRule) otherRule; + + // Possibly in conflict only if the other operation is also on the + // same run target + if (otherCloudRule.getRunTarget().getId().equals(this.getRunTarget().getId()) + && otherCloudRule instanceof StopApplicationSchedulingRule) { + StopApplicationSchedulingRule otherApplicationSchedulingRule = (StopApplicationSchedulingRule) otherCloudRule; + + return otherApplicationSchedulingRule.getAppName().equals(this.getAppName()); + } + } + return false; + } + + public String getAppName() { + return this.appName; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/TargetApplicationsRefreshOperation.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/TargetApplicationsRefreshOperation.java new file mode 100644 index 000000000..393d1ae57 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ops/TargetApplicationsRefreshOperation.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.ops; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.RefreshState; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +/** + * This performs a "two-tier" refresh as fetching list of + * {@link CloudApplication} can be slow, especially if also fetching each app's + * instances. + *

+ * This refresh operation only fetches a "basic" shallow list of + * {@link CloudApplication}, which may be quicker to resolve, and notifies the + * model when element changes occur. + * + *

+ * It also launches a separate refresh job that may take longer to complete + * which is fetching instances and app running state. + * + * @see AppInstancesRefreshOperation + */ +public final class TargetApplicationsRefreshOperation extends CloudOperation { + + private UserInteractions ui; + + public TargetApplicationsRefreshOperation(CloudFoundryBootDashModel model, UserInteractions ui) { + super("Refreshing list of Cloud applications for: " + model.getRunTarget().getName(), model); + this.ui = ui; + } + + @Override + synchronized protected void doCloudOp(IProgressMonitor monitor) throws Exception { + if (model.getRunTarget().isConnected()) { + model.refreshTracker.call("Fetching Apps...", () -> { + for (CloudAppDashElement app : model.getApplicationValues()) { + app.setError(null); // clear old error states. + } + try { + + // 1. Fetch basic list of applications. Should be the "faster" of + // the + // two refresh operations + + List apps = model.getRunTarget().getClient().getApplicationsWithBasicInfo(); + this.model.updateAppNames(getNames(apps)); + + // 2. Launch the slower app stats/instances refresh operation. + this.model.runAsynch(new AppInstancesRefreshOperation(this.model, apps), ui); + return null; + } catch (Exception e) { + /* + * Failed to obtain applications list from CF + */ + model.updateElements(null); + throw e; + } + }); + } else { + model.updateElements(null); + } + } + + private Collection getNames(List apps) { + Builder builder = ImmutableList.builder(); + for (CFApplication app : apps) { + builder.add(app.getName()); + } + return builder.build(); + } + + @Override + public ISchedulingRule getSchedulingRule() { + return new RefreshSchedulingRule(model.getRunTarget()); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiver.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiver.java new file mode 100644 index 000000000..fb4e6670c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiver.java @@ -0,0 +1,269 @@ +/******************************************************************************* + * Copyright (c) 2015, 2018 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.cf.packaging; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.ui.jarpackager.IJarExportRunnable; +import org.eclipse.jdt.ui.jarpackager.JarPackageData; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCallback; +import org.springframework.boot.loader.tools.LibraryScope; +import org.springframework.boot.loader.tools.Repackager; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.ApplicationManifestHandler; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.JavaPackageFragmentRootHandler; +import org.springframework.ide.eclipse.boot.dash.util.UiUtil; +import org.springsource.ide.eclipse.commons.frameworks.core.util.FileUtil; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +public class CloudApplicationArchiver implements ICloudApplicationArchiver { + + private IJavaProject javaProject; + + private String applicationName; + + private final ApplicationManifestHandler parser; + + private static final String TEMP_FOLDER_NAME = "springidetempFolderForJavaAppJar"; + + public CloudApplicationArchiver(IJavaProject javaProject, String applicationName, + ApplicationManifestHandler parser) { + this.javaProject = javaProject; + this.applicationName = applicationName; + this.parser = parser; + } + + public File getApplicationArchive(IProgressMonitor monitor) throws Exception { + File archive = getArchiveFromManifest(monitor); + if (archive == null) { + + File packagedFile = null; + + JavaPackageFragmentRootHandler rootResolver = getPackageFragmentRootHandler(javaProject, monitor); + + final IPackageFragmentRoot[] roots = rootResolver.getPackageFragmentRoots(monitor); + + if (roots == null || roots.length == 0) { + throw ExceptionUtil.coreException("Unable to package project" + javaProject.getElementName() + + " as a jar application. Please verify that the project is a valid Java project and contains a main type in source."); + } + + IType mainType = rootResolver.getMainType(monitor); + + JarPackageData jarPackageData = getJarPackageData(roots, mainType, monitor); + + // generate a manifest file. Note that manifest files + // are only generated in the temporary jar meant for + // deployment. + // The associated Java project is no modified. + jarPackageData.setGenerateManifest(true); + + // This ensures that folders in output folders appear at root + // level + // Example: src/main/resources, which is in the project's + // classpath, contains non-Java templates folder and + // has output folder target/classes. If not exporting output + // folder, + // templates will be packaged in the jar using this path: + // resources/templates + // This may cause problems with the application's dependencies + // if they are looking for just /templates at top level of the + // jar + // If exporting output folders, templates folder will be + // packaged at top level in the jar. + jarPackageData.setExportOutputFolders(true); + + packagedFile = packageApplication(jarPackageData, monitor); + + bootRepackage(roots, packagedFile); + + archive = packagedFile; + } + + return archive; + } + + protected JavaPackageFragmentRootHandler getPackageFragmentRootHandler(IJavaProject javaProject, + IProgressMonitor monitor) throws CoreException { + + return new JavaPackageFragmentRootHandler(javaProject); + } + + protected void bootRepackage(final IPackageFragmentRoot[] roots, File packagedFile) throws Exception { + Repackager bootRepackager = new Repackager(packagedFile); + bootRepackager.repackage(new Libraries() { + + public void doWithLibraries(LibraryCallback callBack) throws IOException { + for (IPackageFragmentRoot root : roots) { + + if (root.isArchive()) { + + File rootFile = new File(root.getPath().toOSString()); + if (rootFile.exists()) { + callBack.library(new Library(rootFile, LibraryScope.COMPILE)); + } + } + } + } + }); + } + + protected JarPackageData getJarPackageData(IPackageFragmentRoot[] roots, IType mainType, IProgressMonitor monitor) + throws Exception { + + String filePath = getTempJarPath(); + + IPath location = new Path(filePath); + + // Note that if no jar builder is specified in the package data + // then a default one is used internally by the data that does NOT + // package any jar dependencies. + JarPackageData packageData = new JarPackageData(); + + packageData.setJarLocation(location); + + // Don't create a manifest. A repackager should determine if a generated + // manifest is necessary + // or use a user-defined manifest. + packageData.setGenerateManifest(false); + + // Since user manifest is not used, do not save to manifest (save to + // manifest saves to user defined manifest) + packageData.setSaveManifest(false); + + packageData.setManifestMainClass(mainType); + packageData.setElements(roots); + return packageData; + } + + protected File packageApplication(final JarPackageData packageData, IProgressMonitor monitor) throws Exception { + + int progressWork = 10; + final SubMonitor subProgress = SubMonitor.convert(monitor, progressWork); + + final File[] createdFile = new File[1]; + + final Exception[] error = new Exception[1]; + Display.getDefault().syncExec(new Runnable() { + + @Override + public void run() { + Shell shell = UiUtil.getShell(); + + IJarExportRunnable runnable = packageData.createJarExportRunnable(shell); + try { + runnable.run(subProgress); + + File file = new File(packageData.getJarLocation().toString()); + createdFile[0] = file; + + } catch (InvocationTargetException e) { + error[0] = e; + } catch (InterruptedException ie) { + error[0] = ie; + } finally { + subProgress.done(); + } + } + + }); + if (error[0] != null) { + throw error[0]; + } + + return createdFile[0]; + } + + public String getTempJarPath() throws Exception { + File tempFolder = FileUtil.createTempDirectory(TEMP_FOLDER_NAME); + tempFolder.delete(); + tempFolder.mkdirs(); + + if (!tempFolder.exists()) { + throw ExceptionUtil.coreException("Failed to create temporary jar file when packaging application for deployment: " + + tempFolder.getAbsolutePath()); + } + + File targetFile = new File(tempFolder, applicationName + ".jar"); + + String path = new Path(targetFile.getAbsolutePath()).toString(); + + System.out.println("getTempJarPath => "+path); + return path; + } + + public File getArchiveFromManifest(IProgressMonitor monitor) throws Exception { + String archivePath = null; + // Read the path again instead of deployment info, as a user may be + // correcting the path after the module was creating and simply + // attempting to push it again without the + // deployment wizard + if (parser.hasManifest()) { + archivePath = parser.getApplicationProperty(applicationName, ApplicationManifestHandler.PATH_PROP, monitor); + } + + File packagedFile = null; + if (archivePath != null) { + // Only support paths that point to archive files + IPath path = new Path(archivePath); + if (path.getFileExtension() != null) { + + if (!path.isAbsolute()) { + // Check if it is project relative first + IFile projectRelativeFile = javaProject.getProject().getFile(path); + if (projectRelativeFile != null && projectRelativeFile.exists()) { + packagedFile = projectRelativeFile.getLocation().toFile(); + } else { + // Case where file exists in file system but is not + // present in workspace (i.e. IProject may be out of + // synch with file system) + IPath projectPath = javaProject.getProject().getLocation(); + if (projectPath != null) { + archivePath = projectPath.append(archivePath).toString(); + File absoluteFile = new File(archivePath); + if (absoluteFile.exists() && absoluteFile.canRead()) { + packagedFile = absoluteFile; + } + } + } + } else { + // See if it is an absolute path + File absoluteFile = new File(archivePath); + if (absoluteFile.exists() && absoluteFile.canRead()) { + packagedFile = absoluteFile; + } + } + } + // If a path is specified but no file found stop further deployment + if (packagedFile == null) { + throw ExceptionUtil.coreException( + "No file found at: " + path + ". Unable to package the application for deployment"); + } else { + return packagedFile; + } + } + return null; + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategies.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategies.java new file mode 100644 index 000000000..2fc7aaca7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategies.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.packaging; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IProgressMonitor; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.ApplicationManifestHandler; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +/** + * Some utilities for creating {@link CloudApplicationArchiverStrategy} instances. + * + * @author Kris De Volder + */ +public class CloudApplicationArchiverStrategies { + + public static CloudApplicationArchiverStrategy justReturn(final ICloudApplicationArchiver archiver) { + return new CloudApplicationArchiverStrategy() { + @Override + public ICloudApplicationArchiver getArchiver(IProgressMonitor mon) { + return archiver; + } + }; + } + + public static CloudApplicationArchiverStrategy fromManifest(IProject project, String appName, ApplicationManifestHandler parser) { + return new CloudApplicationArchiverStrategyFromManifest(project, appName, parser); + } + + public static CloudApplicationArchiverStrategy packageAsJar(IProject project, UserInteractions ui) { + return new CloudApplicationArchiverStrategyAsJar(project, ui); + } + + public static CloudApplicationArchiverStrategy packageMvnAsWar(IProject project, UserInteractions ui) { + return new CloudApplicationArchiverStrategyMavenAsWar(project, ui); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategy.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategy.java new file mode 100644 index 000000000..c73d2ea89 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategy.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.packaging; + +import org.eclipse.core.runtime.IProgressMonitor; + +/** + * Instances of this interface is responsible for initializing and instance of {@link ICloudApplicationArchiver}. + * @author Kris De Volder + */ +public interface CloudApplicationArchiverStrategy { + + /** + * A strategy may or may not always be applicable in a given situation. In that case it may return null when + * asked to produce a ICloudApplicationArchiver. + */ + ICloudApplicationArchiver getArchiver(IProgressMonitor mon); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategyAsJar.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategyAsJar.java new file mode 100644 index 000000000..df5380ebe --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategyAsJar.java @@ -0,0 +1,309 @@ +/******************************************************************************* + * Copyright (c) 2015, 2018 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.cf.packaging; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; +import org.springframework.boot.loader.tools.JarWriter; +import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCallback; +import org.springframework.boot.loader.tools.LibraryScope; +import org.springframework.boot.loader.tools.Repackager; +import org.springframework.ide.eclipse.boot.core.ISpringBootProject; +import org.springframework.ide.eclipse.boot.core.SpringBootCore; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.util.JavaProjectUtil; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.frameworks.core.maintype.MainTypeFinder; +import org.springsource.ide.eclipse.commons.frameworks.core.util.FileUtil; + +public class CloudApplicationArchiverStrategyAsJar implements CloudApplicationArchiverStrategy { + + private static final String TEMP_FOLDER_NAME = "springidetempFolderForJavaAppJar"; + private static final boolean DEBUG = false; + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + /** + * Classpath entries spilt into two lists, one that correspond to the current project's output folders + * and all the others (which correspond to the project's dependencies). The dependencies could be + * jars or output folders for other projects in the workspace. + */ + private static class SplitClasspath { + private List projectContents = new ArrayList<>(2); //one or two is typical + private List dependencies = new ArrayList<>(); + public SplitClasspath(IJavaProject jp, File[] entries) { + Set outputFolders = toFileSet(JavaProjectUtil.getOutputFolders(jp)); + for (File file : entries) { + if (contains(outputFolders, file)) { + projectContents.add(file); + } else { + dependencies.add(file); + } + } + } + + private boolean contains(Set outputFolders, File file) { + return outputFolders.contains(canonical(file)); + } + + private File canonical(File file) { + try { + return file.getCanonicalFile(); + } catch (IOException e) { + //Next best thing: + return file.getAbsoluteFile(); + } + } + + /** + * Convert a collection of Eclipse IContainer to List of java.io.File. Containers that + * don't correspond to stuff on disk are silently ignored. + */ + private Set toFileSet(Set containers) { + Set files = new HashSet<>(); + for (IContainer folder : containers) { + IPath loc = folder.getLocation(); + if (loc!=null) { + File file = loc.toFile(); + files.add(canonical(file)); //use canonical file to make equals / Set work as expected. + } + } + return files; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("SplitClasspath(\n"); + for (File file : projectContents) { + builder.append(" "+file+"\n"); + } + builder.append(" ------------\n"); + for (File file : dependencies) { + builder.append(" "+file+"\n"); + } + builder.append(")"); + return builder.toString(); + } + } + + + + private static final File[] NO_FILES = new File[]{}; + + private static class Archiver implements ICloudApplicationArchiver { + + private IJavaProject jp; + private IType mainType; + private ILaunchConfiguration conf; + private BootLaunchConfigurationDelegate delegate; + private JarNameGenerator jarNames; + private File _tempFolder; + + Archiver(IJavaProject jp, IType mainType) throws CoreException { + this.jp = jp; + this.mainType = mainType; + this.conf = BootLaunchConfigurationDelegate.createWorkingCopy(mainType); + this.delegate = new BootLaunchConfigurationDelegate(); + this.jarNames = new JarNameGenerator(); + } + + private SplitClasspath getRuntimeClasspath() throws CoreException { + return new SplitClasspath(jp, toFiles(delegate.getClasspath(conf))); + } + + private File[] toFiles(String[] classpath) { + if (classpath!=null) { + File[] files = new File[classpath.length]; + for (int i = 0; i < files.length; i++) { + files[i] = new File(classpath[i]); + } + return files; + } + return NO_FILES; + } + + @Override + public File getApplicationArchive(IProgressMonitor mon) throws Exception { + SplitClasspath classpath = getRuntimeClasspath(); + File tempFolder = getTempFolder(); + File baseJar = new File(tempFolder, jp.getElementName()+".original.jar"); + File repackagedJar = new File(tempFolder, jp.getElementName()+".repackaged.jar"); + + createBaseJar(classpath.projectContents, baseJar); + repackage(baseJar, classpath.dependencies, repackagedJar); + return repackagedJar; + } + + private File getTempFolder() throws IOException { + if (_tempFolder==null) { + _tempFolder = FileUtil.createTempDirectory(TEMP_FOLDER_NAME); + } + return _tempFolder; + } + + private void createBaseJar(List projectContents, File baseJar) throws FileNotFoundException, IOException { + JarWriter jarWriter = new JarWriter(baseJar); + try { + for (File outputFolder : projectContents) { + writeFolder(jarWriter, outputFolder); + } + } finally { + jarWriter.close(); + } + } + + private void writeFolder(JarWriter jarWriter, File baseFolder) throws FileNotFoundException, IOException { + for (String name : baseFolder.list()) { + write(jarWriter, baseFolder, name); + } + } + + private void write(JarWriter jarWriter, File baseFolder, String relativePath) throws FileNotFoundException, IOException { + debug("Writing: "+relativePath + " from "+baseFolder); + File file = new File(baseFolder, relativePath); + if (file.isDirectory()) { + debug("Folder"); + for (String name : file.list()) { + write(jarWriter, baseFolder, pathJoin(relativePath, name)); + } + } else if (file.isFile()) { + debug("File"); + jarWriter.writeEntry(relativePath, new FileInputStream(file)); + } else { + debug("Huh?"); + } + } + + private String pathJoin(String relativePath, String name) { + return relativePath + "/" +name; + } + + private void repackage(File baseJar, List dependencies, File repackagedJar) throws IOException { + Repackager repackager = new Repackager(baseJar); + repackager.setMainClass(mainType.getFullyQualifiedName()); + repackager.repackage(repackagedJar, asLibraries(dependencies)); + } + + private Libraries asLibraries(final List dependencies) { + return new Libraries() { + public void doWithLibraries(LibraryCallback callback) throws IOException { + for (File dep : dependencies) { + if (dep.isFile()) { + callback.library(new Library(jarNames.createName(dep), dep, LibraryScope.COMPILE, false)); + } else if (dep.isDirectory()) { + String jarName = jarNames.createName(dep); + File jarFile = new File(getTempFolder(), jarName); + JarWriter jarWriter = new JarWriter(jarFile); + try { + writeFolder(jarWriter, dep); + } finally { + jarWriter.close(); + } + callback.library(new Library(jarName, jarFile, LibraryScope.COMPILE, false)); + } + } + } + }; + } + } + + private SpringBootCore springBootCore = SpringBootCore.getDefault(); + private IProject project; + private UserInteractions ui; + + public CloudApplicationArchiverStrategyAsJar(IProject project, UserInteractions ui) { + this.project = project; + this.ui = ui; + } + + @Override + public ICloudApplicationArchiver getArchiver(IProgressMonitor mon) { + try { + final IJavaProject jp = getJavaProject(); + if (jp!=null && checkPackagingType(jp)) { + final IType type = getMainType(jp, mon); + if (type!=null) { + return new Archiver(jp, type); + } + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + private boolean checkPackagingType(IJavaProject jp) throws CoreException { + ISpringBootProject bootProject = springBootCore.project(jp); + if (bootProject==null) { + //Gradle is poorly supported. We don't know how to determin packaging type. So just + // give such projects the benefit of the doubdt. They *might have the correct + // packaging type. + return true; + } + return ISpringBootProject.PACKAGING_JAR.equals(bootProject.getPackaging()); + } + + private IJavaProject getJavaProject() { + try { + if (project.isAccessible() && project.hasNature(JavaCore.NATURE_ID)) { + return JavaCore.create(project); + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + private IType getMainType(IJavaProject jp, IProgressMonitor mon) { + try { + IType[] candidates = MainTypeFinder.guessMainTypes(jp, mon); + if (candidates!=null && candidates.length>0) { + if (candidates.length==1) { + return candidates[0]; + } else { + //TODO: should persist main type so we don't ask again next time. + // however we persist this, user must be able to change it. + // Prolly we should create a launchconf to store info like this and + // create UI for user to modify it. + return ui.chooseMainType(candidates, "Choose a main type", "Deploying a standalone boot-app requires " + + "that the main type is identified. We found several candidates, please choose one." ); + } + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategyFromManifest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategyFromManifest.java new file mode 100644 index 000000000..b324328ee --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategyFromManifest.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.packaging; + +import java.io.File; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.ApplicationManifestHandler; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +/** + * Archiver strategy that consults manifest.yml file for an entry pointing to an existing archive. + * The existing archive is returned by the archiver rather than building an archive. + */ +public class CloudApplicationArchiverStrategyFromManifest implements CloudApplicationArchiverStrategy { + +// private IProject project; + private String applicationName; + private ApplicationManifestHandler parser; + + public CloudApplicationArchiverStrategyFromManifest(IProject project, String applicationName, ApplicationManifestHandler parser) { +// this.project = project; + this.applicationName = applicationName; + this.parser = parser; + } + + @Override + public ICloudApplicationArchiver getArchiver(IProgressMonitor mon) { + final String archivePath = getArchivePath(mon); + if (archivePath!=null) { + return new ICloudApplicationArchiver() { + public File getApplicationArchive(IProgressMonitor monitor) throws Exception { + return getArchive(archivePath); + } + }; + } + return null; + } + + private String getArchivePath(IProgressMonitor mon) { + if (parser.hasManifest()) { + return parser.getApplicationProperty(applicationName, ApplicationManifestHandler.PATH_PROP, mon); + } + return null; + } + + private File getArchive(String archivePath) throws Exception { + Assert.isNotNull(archivePath); + File packagedFile = null; + // Only support paths that point to archive files + IPath path = new Path(archivePath); + if (path.getFileExtension() != null) { + if (path.isAbsolute()) { + // See if it is an absolute path + File absoluteFile = new File(archivePath); + if (absoluteFile.exists() && absoluteFile.canRead()) { + packagedFile = absoluteFile; + } + } else { + // We'll try and resolve the relative starting from the filesystem directory the manifest itself resides in. + File manifestLocation = parser.getManifestFile(); + if (manifestLocation!=null) { + File baseDir = manifestLocation.getParentFile(); + File absoluteFile = new File(baseDir, archivePath).getAbsoluteFile(); + if (absoluteFile.exists() && absoluteFile.canRead()) { + packagedFile = absoluteFile; + } + } + } + } + // If a path is specified but no file found stop further deployment + if (packagedFile == null) { + throw ExceptionUtil.coreException( + "No file found at: " + path + ". Unable to package the application for deployment"); + } else { + return packagedFile; + } + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategyMavenAsWar.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategyMavenAsWar.java new file mode 100644 index 000000000..b6f534d07 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/CloudApplicationArchiverStrategyMavenAsWar.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.packaging; + +import java.io.File; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.springframework.ide.eclipse.boot.core.ISpringBootProject; +import org.springframework.ide.eclipse.boot.core.SpringBootCore; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.util.Log; + +/** + * Package a boot app to deploy it as a 'war'. + * + * @author Kris De Volder + */ +public class CloudApplicationArchiverStrategyMavenAsWar implements CloudApplicationArchiverStrategy { + + private SpringBootCore springBootCore = SpringBootCore.getDefault(); + private IProject project; +// private UserInteractions ui; + + public CloudApplicationArchiverStrategyMavenAsWar(IProject project, UserInteractions ui) { + this.project = project; +// this.ui = ui; + } + + @Override + public ICloudApplicationArchiver getArchiver(IProgressMonitor mon) { + try { + ISpringBootProject bootProject = springBootCore.project(project); + if (bootProject!=null && ISpringBootProject.PACKAGING_WAR.equals(bootProject.getPackaging())) { + return new WarArchiver(); + } + } catch (CoreException e) { + Log.log(e); + } + return null; + } + + private class WarArchiver implements ICloudApplicationArchiver { + + @Override + public File getApplicationArchive(IProgressMonitor monitor) throws Exception { + ISpringBootProject bootProject = springBootCore.project(project); + if (bootProject!=null) { + return bootProject.executePackagingScript(monitor); + } + return null; + } + + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/ICloudApplicationArchiver.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/ICloudApplicationArchiver.java new file mode 100644 index 000000000..ab8c7a7ac --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/ICloudApplicationArchiver.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.packaging; + +import java.io.File; + +import org.eclipse.core.runtime.IProgressMonitor; + +public interface ICloudApplicationArchiver { + + File getApplicationArchive(IProgressMonitor monitor) throws Exception; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/JarNameGenerator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/JarNameGenerator.java new file mode 100644 index 000000000..b7733c466 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/packaging/JarNameGenerator.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.packaging; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; + +/** + * Responsible for picking the names for dependency jars when packaging a boot app. + *

+ * This class could be turned into an interface with multiple implementations + * should we want to support different naming strategies. But for now there's + * just one implementation, so this is a concrete class. + * + * @author Kris De Volder + */ +public class JarNameGenerator { + + private static final String UNKNOWN_DEPENDENCY = "unknown-dependency"; + private static final IResource[] NO_RESOURCES = {}; + private Set used = new HashSet(); + + private final String EXTENSION = ".jar"; + + public String createName(File dep) { + String candidate = getNameCandidate(dep); + return makeUnique(candidate); + } + + private String makeUnique(String candidate) { + if (candidate.toLowerCase().endsWith(EXTENSION)) { + candidate = candidate.substring(0, candidate.length()-EXTENSION.length()); + } + String name = candidate + EXTENSION; + int id = 0; + while (used.contains(name)) { + name = candidate+"-"+(++id) + EXTENSION; + } + used.add(name); + return name; + } + + protected String getNameCandidate(File dep) { + if (isJar(dep)) { + return getNameCandidateForJar(dep); + } else if (dep.isDirectory()) { + return getNameCandidateForDir(dep); + } + //Shouldn't happen, but return something anyway + return UNKNOWN_DEPENDENCY; + } + + private String getNameCandidateForDir(File dep) { + //We'll try to use the name of enclosing workspace project if possible + IProject p = getEnclosingProject(dep); + if (p!=null) { + return p.getName(); + } + return UNKNOWN_DEPENDENCY; + } + + protected IProject getEnclosingProject(File dep) { + IResource best = chooseBest(getResources(dep)); + if (best!=null) { + return best.getProject(); + } + return null; + } + + /** + * If more than one resource matched a dependency we try to find the + * 'best' resource. We do this by looking at the path relative to the workspace + * and pick the one with the shortest path. This way we will pick the resource + * in the most nested project (i.e. when projects are nested a resource + * may exists both in the parent and child projects. We are interested in + * the child in this case. + */ + protected IResource chooseBest(IResource[] resources) { + int bestLen = Integer.MAX_VALUE; + IResource best = null; + for (IResource r : resources) { + int len = r.getFullPath().segmentCount(); + if (len < bestLen) { + best = r; + bestLen = len; + } + } + return best; + } + + protected IResource[] getResources(File dep) { + if (dep.isDirectory()) { + return getWorkspaceRoot().findContainersForLocationURI(dep.toURI()); + } else if (dep.isFile()) { + return getWorkspaceRoot().findFilesForLocationURI(dep.toURI()); + } + return NO_RESOURCES; + } + + protected IWorkspaceRoot getWorkspaceRoot() { + return ResourcesPlugin.getWorkspace().getRoot(); + } + + protected String getNameCandidateForJar(File dep) { + return dep.getName(); + } + + protected boolean isJar(File dep) { + String name = dep.getName(); + return name.endsWith(".jar") || name.endsWith(".JAR"); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/ParsedUri.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/ParsedUri.java new file mode 100644 index 000000000..6e63c4daa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/ParsedUri.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.routes; + +/** + * Represents the result of 'parsing; a route uri by a 'dumb' parser. + *

+ * Dumb parser is unaware of dynamic infos about existing domains and so it + * doesn't have the ability to split host and domain name apart as the uri + * syntax alone is not sufficient to determine where the host-name ends and + * the domain name begins. + * + * @author Kris De Volder + */ +public class ParsedUri { + + private String hostAndDomain; // These aren't split apart by the parser, because a dumb parser can't know where to split + private String path; + private Integer port; + + public ParsedUri(String uri) { + //Format: ${hostAndDomain}:${port}/${path} (where the slash is considered a part of the path) + int slash = uri.indexOf('/'); + if (slash>=0) { + path = uri.substring(slash); + uri = uri.substring(0, slash); + } + int colon = uri.indexOf(':'); + if (colon>=0) { + port = Integer.parseInt(uri.substring(colon+1)); + uri = uri.substring(0, colon); + } + hostAndDomain = uri; + } + + @Override + public String toString() { + return "ParsedUri [hostAndDomain=" + hostAndDomain + ", path=" + path + ", port=" + port + "]"; + } + + public String getHostAndDomain() { + return hostAndDomain; + } + + public String getPath() { + return path; + } + + public Integer getPort() { + return port; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/Randomized.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/Randomized.java new file mode 100644 index 000000000..ad3633289 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/Randomized.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.routes; + +import org.eclipse.core.runtime.Assert; + +/** + * Represents an intention to set a value to either: + *

    + *
  • a specific value, or + *
  • a randomly chosen value. + *
+ */ +public class Randomized { + + private final T fixedValue; + + private Randomized(T v) { + this.fixedValue = v; + } + + public boolean isRandom() { + return this.fixedValue==null; + } + + public T getValue() { + Assert.isNotNull(this.fixedValue); + return this.fixedValue; + } + + public static Randomized value(T value) { + Assert.isNotNull(value); + return new Randomized<>(value); + } + + public static Randomized random() { + return new Randomized<>(null); + } + + @Override + public String toString() { + if (isRandom()) { + return "?"; + } else { + return getValue().toString(); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((fixedValue == null) ? 0 : fixedValue.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Randomized other = (Randomized) obj; + if (fixedValue == null) { + if (other.fixedValue != null) + return false; + } else if (!fixedValue.equals(other.fixedValue)) + return false; + return true; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/RouteAttributes.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/RouteAttributes.java new file mode 100644 index 000000000..bded05338 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/RouteAttributes.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.routes; + +import java.util.List; + +import org.springframework.ide.eclipse.boot.dash.cf.deployment.YamlGraphDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeploymentProperties; + +/** + * Data object containing attributes from a manifest.yml related to route bindings. + * (i.e. any attribute in the manifest.yml file that potentially affects the routes + * that will be bound to an app on push, is included here. + *

+ * This is intented to be a 'dumb' data object. It should have no smarts around interpreting the + * meanings of attributes. Its only purpose is to be a container for + * exactly the values as found in the manifest.mf file. + * + * @author Kris De Volder + */ +public class RouteAttributes { + + private final String appName; + private final List routes; + private final String host; + private final List hosts; + private final String domain; + private final List domains; + private final boolean noHost; + private final boolean noRoute; + private final boolean randomRoute; + + public RouteAttributes(YamlGraphDeploymentProperties manifest) { + this.appName = manifest.getAppName(); + this.routes = manifest.getRoutes(); + this.domain = manifest.getRawDomain(); + this.domains = manifest.getRawDomains(); + this.host = manifest.getRawHost(); + this.hosts = manifest.getRawHosts(); + this.noRoute = manifest.getRawNoRoute(); + this.randomRoute = manifest.getRawRandomRoute(); + this.noHost = manifest.getRawNoHost(); + } + + public List getRoutes() { + return routes; + } + public String getHost() { + return host; + } + public List getHosts() { + return hosts; + } + public String getDomain() { + return domain; + } + public List getDomains() { + return domains; + } + public boolean isNoHost() { + return noHost; + } + public boolean isNoRoute() { + return noRoute; + } + public boolean isRandomRoute() { + return randomRoute; + } + public String getAppName() { + return appName; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/RouteBinding.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/RouteBinding.java new file mode 100644 index 000000000..a1d8e6f21 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/RouteBinding.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.routes; + +public class RouteBinding { + + private Randomized host; + private String domain; + private Randomized port; + private String path; + + public Randomized getHost() { + return host; + } + + public void setHost(String host) { + setHost(host==null ? null : Randomized.value(host)); + } + public void setHost(Randomized host) { + this.host = host; + } + + public Randomized getPort() { + return port; + } + public void setPort(Randomized port) { + this.port = port; + } + public void setPort(Integer port) { + setPort(port == null ? null : Randomized.value(port)); + } + public String getDomain() { + return domain; + } + public void setDomain(String domain) { + this.domain = domain; + } + + public String getPath() { + return path; + } + + /** + * This toString returns something that is not quite a uri. The '.' separating host from + * domain is replaced by '@'. This is so we can actually see where the host ends and the + * domain name starts (which you can't in a uri, since host and domains may both contain + * dots. + */ + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + if (host!=null) { + s.append(getHost()); + s.append('@'); + } + s.append(getDomain()); + if (port!=null) { + s.append(':'); + s.append(getPort()); + } + if (path!=null) { + s.append(getPath()); + } + return s.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((domain == null) ? 0 : domain.hashCode()); + result = prime * result + ((host == null) ? 0 : host.hashCode()); + result = prime * result + ((port == null) ? 0 : port.hashCode()); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RouteBinding other = (RouteBinding) obj; + if (domain == null) { + if (other.domain != null) + return false; + } else if (!domain.equals(other.domain)) + return false; + if (host == null) { + if (other.host != null) + return false; + } else if (!host.equals(other.host)) + return false; + if (port == null) { + if (other.port != null) + return false; + } else if (!port.equals(other.port)) + return false; + return true; + } + + public String toUri() { + StringBuilder s = new StringBuilder(); + if (host!=null) { + s.append(host); + s.append('.'); + } + s.append(domain); + if (port!=null) { + s.append(':'); + s.append(port); + } + if (path!=null) { + if (!path.startsWith("/")) { + s.append('/'); + } + s.append(path); + } + return s.toString(); + } + + public void setPath(String path) { + this.path = path; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/RouteBuilder.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/RouteBuilder.java new file mode 100644 index 000000000..0f57096c2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/routes/RouteBuilder.java @@ -0,0 +1,241 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.routes; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFDomainType; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFDomainStatus; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import com.google.common.collect.ImmutableMap; + +/** + * This class implements the logics of translating from the multitude of attributes in a + * `manifest.mf` like 'random-route', 'no-host', 'domain', 'domains', etc. into concrete + * 'route bindings' that can be passed of to a lower-level cf client operation such as `mapRoute` + * to bind a route to an app. + *

+ * This process is somewhat complex because of the multitude of attributes, and their interactions, + * as well as different interpretations of some attributes depending on the target domain being a + * TCP vs HTTP domain. + * + * @author Kris De Volder + */ +public class RouteBuilder { + + private Map domainsByName = new LinkedHashMap<>(); + private String defaultDomain; + + public RouteBuilder(Collection domains) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (CFCloudDomain d : domains) { + builder.put(d.getName(), d); + } + domainsByName = builder.build(); + } + + /** + * Builds the list of route bindings from a parsed manifest.yml's attributes. + *

+ * The routes builder makes implicit assumptions that the manifest doesn't have + * conflicting attributes (e.g. `host` and `random-route`). + *

+ * If conflicting attributes *are* present then the routes builder will ignore + * some of the conflicting attributes in a somewhat arbitrary way. So it will still + * compute a list of route bindings rather than raise an exception. + *

+ * A similar approach is taken for 'routes' definitions that can't be mapped onto + * the list of known domains in a valid way. These routes will simply be silently + * ignored. + */ + public List buildRoutes(RouteAttributes manifest) { + if (manifest.isNoRoute()) { + return ImmutableList.of(); + } + if (manifest.isRandomRoute()) { + return buildRandomRoutes(manifest); + } + if (manifest.getRoutes()!=null) { + return buildRoutesFromUris(manifest); + } + return buildRoutesFromHostsAndDomains(manifest); + } + + private List buildRoutesFromHostsAndDomains(RouteAttributes manifest) { + if (manifest.isNoHost()) { + List domains = getDomains(manifest); + ImmutableList.Builder routes = ImmutableList.builder(); + for (String domain : domains) { + RouteBinding route = new RouteBinding(); + route.setDomain(domain); + routes.add(route); + } + return routes.build(); + } else { + List hosts = getHosts(manifest); + List domains = getDomains(manifest); + ImmutableList.Builder routes = ImmutableList.builder(); + for (String host : hosts) { + for (String domain : domains) { + RouteBinding route = new RouteBinding(); + route.setHost(host); + route.setDomain(domain); + routes.add(route); + } + } + return routes.build(); + } + } + + protected List buildRoutesFromUris(RouteAttributes manifest) { + Builder builder = ImmutableList.builder(); + for (String desiredUri : manifest.getRoutes()) { + RouteBinding route = buildRouteFromUri(desiredUri, manifest); + if (route!=null) { + builder.add(route); + } + } + return builder.build(); + } + + protected List buildRandomRoutes(RouteAttributes manifest) { + Builder builder = ImmutableList.builder(); + List domains = getDomains(manifest); + for (String domain : domains) { + RouteBinding route = new RouteBinding(); + route.setDomain(domain); + if (isTcpDomain(domain)) { + route.setPort(Randomized.random()); + } else { + route.setHost(Randomized.random()); + } + builder.add(route); + } + return builder.build(); + } + + private boolean isTcpDomain(String domainName) { + CFCloudDomain d = domainsByName.get(domainName); + if (d!=null) { + return d.getType()==CFDomainType.TCP; + } + return false; + } + + private List getHosts(RouteAttributes manifest) { + Set builder = new LinkedHashSet<>(); + String host = manifest.getHost(); + if (host!=null) { + builder.add(host); + } + List hosts = manifest.getHosts(); + if (hosts!=null) { + builder.addAll(hosts); + } + if (builder.isEmpty()) { + String appName = manifest.getAppName(); + if (appName!=null) { + builder.add(appName); + } + } + return ImmutableList.copyOf(builder); + } + + private List getDomains(RouteAttributes manifest) { + Set domains = new LinkedHashSet<>(); + List ds = manifest.getDomains(); + if (ds!=null) { + domains.addAll(ds); + } + String d = manifest.getDomain(); + if (d!=null) { + domains.add(d); + } + if (domains.isEmpty()) { + //This assumes 'getDomains' is only called in context where we actually + // want the domains from the 'domain' or 'domains' attributes. So, not + // for example, in a manifest with a 'routes' or a 'no-routes'. + //In these contexts, of the list of domains is empty, we generally want + // to fallback to using the defaultDomain. + String dflt = getDefaultDomain(); + if (dflt!=null) { + domains.add(dflt); + } + } + return ImmutableList.copyOf(domains); + } + + /** + * Create a RouteBinding from a 'target' uri. Such a routebinding is always specific + * and doesn't contain randomized components (i.e. no random host/port). + */ + private RouteBinding buildRouteFromUri(String _uri, RouteAttributes args) { + ParsedUri uri = new ParsedUri(_uri); + CFCloudDomain bestDomain = domainsByName.values().stream() + .filter(domain -> matches(domain, uri)) + .max((d1, d2) -> Integer.compare(d1.getName().length(), d2.getName().length())) + .orElse(null); + if (bestDomain!=null) { + RouteBinding route = new RouteBinding(); + route.setDomain(bestDomain.getName()); + route.setHost(bestDomain.splitHost(uri.getHostAndDomain())); + route.setPath(uri.getPath()); + route.setPort(uri.getPort()); + return route; + } + return null; + } + + public String getDefaultDomain() { + if (defaultDomain==null) { + defaultDomain = getDomains().stream() + .filter(d -> d.getStatus()==CFDomainStatus.SHARED && d.getType()==CFDomainType.HTTP) + .findFirst() + .map(d -> d.getName()) + .orElse(null); + } + return defaultDomain; + } + + private Collection getDomains() { + return domainsByName.values(); + } + + /** + * Determines whether a given domain can be used to construct a route for a given + * target uri. This check is strictly based on the names of the domain and target uri, + * type of the domain/uri doesn't play into it (so, for example there are no 'smarts' here + * to detect whether or not a domain's type is compatible with the uri type). + */ + private boolean matches(CFCloudDomain domainData, ParsedUri uri) { + String domain = domainData.getName(); + String hostAndDomain = uri.getHostAndDomain(); + if (!hostAndDomain.endsWith(domain)) { + return false; + } + if (domain.length()==hostAndDomain.length()) { + //The uri matches domain precisely + return true; + } else if (hostAndDomain.charAt(hostAndDomain.length()-domain.length()-1)=='.') { + //The uri matches as ${host}.${domain} + return true; + } + return false; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CfTargetsInfo.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CfTargetsInfo.java new file mode 100644 index 000000000..66e20b5da --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CfTargetsInfo.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) 2018 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.runtarget; + +import java.util.List; + +/** + * JSON-friendly representation of Cloud Foundry targets, used for integrations that need + * to serialise CF target information for transmission (e.g. between a client and server) + *

+ * WARNING: As this is used to serialise and deserialise between client and server, this exact type is used on both client and server. + * Therefore this type should always match the one in the language server. For example, see the identical copy of this class in: + * org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfo + * + */ +public class CfTargetsInfo { + + private List cfTargets; + private TargetDiagnosticMessages diagnosticMessages; + + public List getCfTargets() { + return cfTargets; + } + + public void setCfTargets(List cfTargets) { + this.cfTargets = cfTargets; + } + + public TargetDiagnosticMessages getDiagnosticMessages() { + return diagnosticMessages; + } + + public void setDiagnosticMessages(TargetDiagnosticMessages diagnosticMessages) { + this.diagnosticMessages = diagnosticMessages; + } + + public static class TargetDiagnosticMessages { + + + private String noTargetsFound; + private String connectionError; + private String noOrgSpace; + private String targetSource; + + /** + * + * @return only when there are no targets available (e.g. cf CLI is not connected or no boot dash targets) + */ + public String getNoTargetsFound() { + return noTargetsFound; + } + + public void setNoTargetsFound(String noTargetsFound) { + this.noTargetsFound = noTargetsFound; + } + + /** + * + * @return error if any existing target cannot connect. + */ + public String getConnectionError() { + return connectionError; + } + + public void setConnectionError(String connectionError) { + this.connectionError = connectionError; + } + + public String getNoOrgSpace() { + return noOrgSpace; + } + + public void setNoOrgSpace(String noOrgSpace) { + this.noOrgSpace = noOrgSpace; + } + + public String getTargetSource() { + return targetSource; + } + + public void setTargetSource(String targetSource) { + this.targetSource = targetSource; + } + } + + public static class Target { + private String api; + private String org; + private String space; + + private boolean sslDisabled; + private String refreshToken; + + public String getApi() { + return api; + } + + public void setApi(String api) { + this.api = api; + } + + public String getOrg() { + return org; + } + + public void setOrg(String org) { + this.org = org; + } + + public void setSpace(String space) { + this.space = space; + } + + public String getSpace() { + return space; + } + + public boolean getSslDisabled() { + return sslDisabled; + } + + public void setSslDisabled(boolean sslDisabled) { + this.sslDisabled = sslDisabled; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + @Override + public String toString() { + return "Target [api=" + api + ", org=" + org + ", space=" + space + "]"; + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CloudFoundryRunTarget.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CloudFoundryRunTarget.java new file mode 100644 index 000000000..181ba0589 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CloudFoundryRunTarget.java @@ -0,0 +1,458 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.runtarget; + +import static org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.BootDashCfColumns.JMX_SSH_TUNNEL; +import static org.springframework.ide.eclipse.boot.dash.model.RunState.DEBUGGING; +import static org.springframework.ide.eclipse.boot.dash.model.RunState.INACTIVE; +import static org.springframework.ide.eclipse.boot.dash.model.RunState.RUNNING; +import static org.springframework.ide.eclipse.boot.dash.model.RunState.STARTING; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.DEFAULT_PATH; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.INSTANCES; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.NAME; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.PROJECT; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.RUN_STATE_ICN; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.TAGS; + +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.osgi.framework.Version; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.DebuggableTarget; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFBuildpack; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFSpace; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFStack; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.CloudFoundryClientFactory; +import org.springframework.ide.eclipse.boot.dash.cf.client.SshClientSupport; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.PasswordDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.StoreCredentialsMode; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.ui.CfUserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.AbstractBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.AbstractRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.RunTargetWithProperties; +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.RemoteRunTarget; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.util.Log; +import org.springsource.ide.eclipse.commons.livexp.util.OldValueDisposer; +import org.springsource.ide.eclipse.commons.ui.UiUtil; + +import com.google.common.collect.ImmutableSet; + +public class CloudFoundryRunTarget extends AbstractRunTarget +implements RunTargetWithProperties, RemoteRunTarget, DebuggableTarget { + + private CloudFoundryTargetProperties targetProperties; + + // Cache these to avoid frequent client calls + private List domains; + private List spaces; + private List stacks; + + private OldValueDisposer cachedClientDisposer; + private CloudFoundryClientFactory clientFactory; + + public CloudFoundryRunTarget(CloudFoundryTargetProperties targetProperties, CloudFoundryRunTargetType runTargetType, CloudFoundryClientFactory clientFactory) { + super(runTargetType, CloudFoundryTargetProperties.getId(targetProperties), + CloudFoundryTargetProperties.getName(targetProperties)); + this.targetProperties = targetProperties; + this.clientFactory = clientFactory; + this.cachedClientDisposer = new OldValueDisposer<>(this); + cachedClient().onChange((_e, v) -> { + try { + if (getClient() != null) { + persistBuildpacks(getClient().getBuildpacks()); + } + } catch (Exception e) { + Log.log(e); + } + }); + } + + private LiveVariable cachedClient() { + return cachedClientDisposer.getVar(); + } + + public static final EnumSet RUN_GOAL_STATES = EnumSet.of(INACTIVE, STARTING, RUNNING, DEBUGGING); + private static final BootDashColumn[] DEFAULT_COLUMNS = { RUN_STATE_ICN, NAME, PROJECT, INSTANCES, DEFAULT_PATH, TAGS, JMX_SSH_TUNNEL }; + + private static final String APPS_MANAGER_HOST = "APPS_MANAGER_HOST"; + private static final String BUILDPACKS = "BUILDPACKS"; + + @Override + public ClientRequests getClient() { + return cachedClient().getValue(); + } + + @Override + public void connect(ConnectMode mode) throws Exception { + try { + this.domains = null; + this.spaces = null; + this.stacks = null; + boolean createClient = getTargetProperties().getCredentials()!=null; + if (mode==ConnectMode.INTERACTIVE && getTargetProperties().getCredentials()==null) { + updatePasswordAndConnect(); + } + if (createClient) { + cachedClient().setValue(createClient()); + } + } catch (Exception e) { + cachedClient().setValue(null); + throw e; + } + } + + private CfUserInteractions cfUi() { + return getType().injections().getBean(CfUserInteractions.class); + } + + private UserInteractions ui() { + return getType().injections().getBean(UserInteractions.class); + } + + public boolean updatePasswordAndConnect() throws Exception { + final StoreCredentialsMode storePassword = this.getTargetProperties().getStoreCredentials(); + PasswordDialogModel passwordDialogModel = new PasswordDialogModel(this.getClientFactory(), this.getTargetProperties(), storePassword); + cfUi().openPasswordDialog(passwordDialogModel); + if (passwordDialogModel.isOk()) { + this.getTargetProperties().setStoreCredentials( + passwordDialogModel.getEffectiveStoreMode()); + CFCredentials credentials = passwordDialogModel.getCredentials(); + // The credentials cannot be null or empty string - enforced by the dialog + try { + this.getTargetProperties().setCredentials(credentials); + cachedClient().setValue(createClient()); + } catch (CannotAccessPropertyException e) { + ui().warningPopup("Failed Storing Password", + "Failed to store password in Secure Storage for " + this.getId() + + ". Secure Storage is most likely locked. Current password will be kept until disconnect."); + // Set "remember password" to false. Password hasn't been stored. + this.getTargetProperties().setStoreCredentials(StoreCredentialsMode.STORE_NOTHING); + } + return true; + } + return false; + } + + @Override + public void disconnect() { + this.domains = null; + this.spaces = null; + this.stacks = null; + cachedClient().setValue(null); + } + @Override + public void dispose() { + disconnect(); + super.dispose(); + } + + protected void persistBuildpacks(List buildpacks) throws Exception { + PropertyStoreApi properties = getPersistentProperties(); + + if (properties != null) { + + String[] buildpackVals = null; + if (buildpacks != null && !buildpacks.isEmpty()) { + buildpackVals = new String[buildpacks.size()]; + for (int i = 0; i < buildpacks.size(); i++) { + buildpackVals[i] = buildpacks.get(i).getName(); + } + } + + properties.put(BUILDPACKS, buildpackVals); + } + } + + @Override + public boolean isConnected() { + return cachedClient().getValue() != null; + } + + public Version getCCApiVersion() { + try { + ClientRequests client = getClient(); + if (client!=null) { + return client.getApiVersion(); + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + public CloudFoundryClientFactory getClientFactory() { + return clientFactory; + } + + protected ClientRequests createClient() throws Exception { + return clientFactory.getClient(this.getTargetProperties()); + } + + @Override + public BootDashColumn[] getDefaultColumns() { + return DEFAULT_COLUMNS; + } + + @Override + public AbstractBootDashModel createSectionModel(BootDashViewModel parent) { + return new CloudFoundryBootDashModel(this, parent.getContext(), parent); + } + + @Override + public CloudFoundryTargetProperties getTargetProperties() { + return targetProperties; + } + + @Override + public boolean canRemove() { + return true; + } + + @Override + public boolean canDeployAppsFrom() { + return false; + } + + @Override + public boolean requiresCredentials() { + return true; + } + + public synchronized List getDomains( IProgressMonitor monitor) + throws Exception { + if (domains == null) { + SubMonitor subMonitor = SubMonitor.convert(monitor, 10); + subMonitor.beginTask("Refreshing list of domains for " + getName(), 5); + + domains = getClient().getDomains(); + + subMonitor.worked(5); + } + return domains; + } + + public synchronized List getStacks(IProgressMonitor monitor) + throws Exception { + if (stacks == null) { + SubMonitor subMonitor = SubMonitor.convert(monitor, 10); + subMonitor.beginTask("Refreshing list of stacks for " + getName(), 5); + + stacks = getClient().getStacks(); + + subMonitor.worked(5); + } + return stacks; + } + + public synchronized List getSpaces(ClientRequests requests, IProgressMonitor monitor) throws Exception { + if (spaces == null) { + SubMonitor subMonitor = SubMonitor.convert(monitor, 10); + + subMonitor.beginTask("Refreshing list of spaces for " + getName(), 5); + spaces = requests.getSpaces(); + subMonitor.worked(5); + } + return spaces; + } + + public boolean isPWS() { + return "https://api.run.pivotal.io".equals(getUrl()); + } + + public String getUrl() { + String url = targetProperties.getUrl(); + while (url.endsWith("/")) { + url = url.substring(0, url.length()-1); + } + return url; + } + + public SshClientSupport getSshClientSupport() throws Exception { + ClientRequests client = getClient(); + return client.getSshClientSupport(); + } + + /** + * Returns list of cached buildpacks. It does NOT fetch updated buildpacks from CF as this method may + * be called in cases where fast list of buildpacks is required (e.g values that show in pop-up UI). + *

+ * Do NOT use this method to fetch buildpacks from CF + * @return Collection of CACHED buildpacks, or null if none cached yet. + * @throws Exception + */ + public Collection getBuildpackValues() throws Exception { + PropertyStoreApi properties = getPersistentProperties(); + if (properties != null) { + String[] buildPackVals = properties.get(BUILDPACKS, (String[]) null); + if (buildPackVals != null) { + return Arrays.asList(buildPackVals); + } + } + return null; + } + + public String getBuildpack(IProject project) { + // Only support Java project for now + IJavaProject javaProject = JavaCore.create(project); + + if (javaProject != null) { + try { + + + Collection buildpacks = getBuildpackValues(); + if (buildpacks != null) { + String javaBuildpack = null; + // Only chose a java build iff ONE java buildpack exists + // that contains the java_buildpack pattern. + + for (String bp : buildpacks) { + // iterate through all buildpacks to make sure only + // ONE java buildpack exists + if (bp.contains("java_buildpack")) { + if (javaBuildpack == null) { + javaBuildpack = bp; + } else { + // If more than two buildpacks contain + // "java_buildpack", do not chose any. Let CF buildpack + // detection decided which one to chose. + javaBuildpack = null; + break; + } + } + } + return javaBuildpack; + } + } catch (Exception e) { + Log.log(e); + } + } + + return null; + } + + @Override + public String getTemplateVar(char name) { + switch (name) { + case 'o': + return getTargetProperties().getOrganizationName(); + case 's': + return getTargetProperties().getSpaceName(); + case 'a': + return getTargetProperties().getUrl(); + case 'u': + return getTargetProperties().getUsername(); + default: + return super.getTemplateVar(name); + } + } + + public String getAppsManagerHost() { + PropertyStoreApi props = getPersistentProperties(); + if (props != null) { + String appsManagerURL = props.get(APPS_MANAGER_HOST); + if (appsManagerURL != null) { + return appsManagerURL; + } + } + return getAppsManagerHostDefault(); + } + + public void setAppsManagerHost(String appsManagerURL) throws Exception { + getPersistentProperties().put(APPS_MANAGER_HOST, appsManagerURL); + } + + public String getAppsManagerURL() { + String host = getAppsManagerHost(); + CloudFoundryTargetProperties targetProperties = getTargetProperties(); + + String org = targetProperties.getOrganizationGuid(); + String space = targetProperties.getSpaceGuid(); + + if (host != null && host.length() > 0 && org != null && org.length() > 0 && space != null && space.length() > 0) { + return host + "/organizations/" + org + "/spaces/" + space; + } else { + return null; + } + } + + public String getAppsManagerHostDefault() { + String url = getUrl(); + if (url != null && url.contains("//api.")) { + return url.replace("//api.", "//console."); + } + else { + return null; + } + } + + @Override + public LiveExpression getClientExp() { + return cachedClient(); + } + + @Override + public void performDoubleClickAction(UserInteractions ui) { + openCloudAdminConsole(ui); + } + + public void openCloudAdminConsole(UserInteractions ui) { + String appsManagerURL = getAppsManagerURL(); + if (appsManagerURL != null && appsManagerURL.length() > 0) { + UiUtil.openUrl(appsManagerURL); + } + else { + ui.errorPopup("can't find unique identificators", + "The Cloud Target that you selected doesn't contain required information about the organization and the space yet (recently added unique identifiers). Please remove the target and add it again to fix this."); + } + } + + @Override + public CloudFoundryTargetProperties getParams() { + return this.targetProperties; + } + + @Override + public Collection fetchApps() { + //TODO: migrate CF to use GenericRemoteBootDashModel. And then this should be implement based on + // how CFBootDashModel fetches apps. + return ImmutableSet.of(); + } + + @Override + public CloudFoundryRunTargetType getType() { + return (CloudFoundryRunTargetType) super.getType(); + } + + @Override + public boolean isDebuggingSupported() { + return true; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CloudFoundryRunTargetType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CloudFoundryRunTargetType.java new file mode 100644 index 000000000..56dad4668 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CloudFoundryRunTargetType.java @@ -0,0 +1,194 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.cf.runtarget; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.swt.widgets.Shell; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.client.CloudFoundryClientFactory; +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshTunnelFactory; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.CloudFoundryTargetWizardModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.RunTargetWizard; +import org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.JmxSshTunnelManager; +import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsUtil; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +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.DefaultWizardModelUserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.MissingLiveInfoMessages; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.WizardModelUserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.AbstractRemoteRunTargetType; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.TargetProperties; +import org.springframework.ide.eclipse.boot.dash.util.UiUtil; +import org.springframework.ide.eclipse.boot.util.ProcessTracker; +import org.springframework.ide.eclipse.editor.support.util.HtmlSnippet; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.gson.Gson; + +/** + * @author Kris De Volder + */ +public class CloudFoundryRunTargetType extends AbstractRemoteRunTargetType implements Disposable { + + private static final ImageDescriptor SMALL_ICON = BootDashActivator.getImageDescriptor("icons/cloud_obj.png"); + + private WizardModelUserInteractions interactions; + + public CloudFoundryRunTargetType(SimpleDIContext injections) { + super(injections, "Cloud Foundry"); + injections.assertDefinitionFor(SshTunnelFactory.class); + injections.assertDefinitionFor(JmxSshTunnelManager.class); + + //TODO: Should be injected and merged with other user interactions, but required too much + // refactoring to implement in limited time + this.interactions = new DefaultWizardModelUserInteractions(); + } + + @Override + public CompletableFuture openTargetCreationUi(LiveSetVariable targets) { + try { + CloudFoundryTargetWizardModel model = new CloudFoundryTargetWizardModel(this, clientFactory(), + targets.getValues(), context(), interactions); + RunTargetWizard wizard = new RunTargetWizard(model); + Shell shell = UiUtil.getShell(); + if (shell != null) { + WizardDialog dialog = new WizardDialog(shell, wizard); + if (dialog.open() == Dialog.OK) { + CloudFoundryRunTarget target = wizard.getRunTarget(); + if (target != null) { + targets.add(target); + } + } + } + return CompletableFuture.completedFuture(null); + } catch (Exception e) { + Log.log(e); + CompletableFuture f = new CompletableFuture(); + f.completeExceptionally(e); + return f; + } + } + + + + private CloudFoundryClientFactory clientFactory() { + return injections.getBean(CloudFoundryClientFactory.class); + } + + private BootDashModelContext context() { + return injections.getBean(BootDashModelContext.class); + } + + @Override + public RunTarget createRunTarget(CloudFoundryTargetProperties props) { + return new CloudFoundryRunTarget((CloudFoundryTargetProperties) props, this, clientFactory()); + } + + @Override + public ImageDescriptor getIcon() { + return BootDashActivator.getImageDescriptor("icons/cloud-ready.png"); + } + @Override + public ImageDescriptor getDisconnectedIcon() { + return BootDashActivator.getImageDescriptor("icons/cloud-inactive.png"); + } + + @Override + public String getDefaultNameTemplate() { + return "%o : %s - [%a]"; + } + + @Override + public String getTemplateHelpText() { + return + "Enter a template pattern. The following variable substitution are defined:\n" + + " '%u': username\n" + + " '%o': organization\n" + + " '%s': space\n" + + " '%a': API URL\n" + + "\n" + + "To escape a variable name simply repeat the '%' sign. E.g. '%%u'"; + } + + @Override + public CloudFoundryTargetProperties parseParams(String serializedTargetParams) { + Gson gson = new Gson(); + @SuppressWarnings("unchecked") + Map map = gson.fromJson(serializedTargetParams, Map.class); + return new CloudFoundryTargetProperties(new TargetProperties(map, this), this, injections); + } + + @Override + public String serialize(CloudFoundryTargetProperties props) { + return props.toJson(); + } + + @Override + public UserInteractions ui() { + // TODO Auto-generated method stub + return null; + } + + @Override + public MissingLiveInfoMessages getMissingLiveInfoMessages() { + return new MissingLiveInfoMessages() { + @Override + public HtmlSnippet getMissingInfoMessage(String appName, String actuatorEndpoint) { + + return buffer -> { + buffer.raw("

"); + buffer.raw(""); + buffer.text(appName); + buffer.raw(""); + buffer.text(" must be running with JMX and actuator endpoint enabled:"); + buffer.raw("

"); + + buffer.raw("
    "); + + buffer.raw("
  1. "); + buffer.text("Enable actuator "); + buffer.raw(""); + buffer.text(actuatorEndpoint); + buffer.raw(""); + buffer.text(" endpoint in the application."); + buffer.raw("
  2. "); + + buffer.raw("
  3. "); + buffer.text("Select "); + buffer.raw(""); + buffer.text("Enable JMX SSH Tunnel"); + buffer.raw(""); + buffer.text(" in the deployment dialog."); + buffer.raw("
  4. "); + buffer.raw("
"); + + buffer.href(EXTERNAL_DOCUMENT_LINK, "See documentation"); + }; + + } + }; + } + + @Override + public void dispose() { + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CloudFoundryTargetProperties.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CloudFoundryTargetProperties.java new file mode 100644 index 000000000..de0bb1674 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/runtarget/CloudFoundryTargetProperties.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.cf.runtarget; + +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFClientParams; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFSpace; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.StoreCredentialsMode; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModelContext; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.CannotAccessPropertyException; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.TargetProperties; + +import com.google.gson.Gson; + +public class CloudFoundryTargetProperties extends TargetProperties { + + public final static String ORG_PROP = "organization"; + public final static String SPACE_PROP = "space"; + public final static String SELF_SIGNED_PROP = "selfsigned"; + public final static String SKIP_SSL_VALIDATION_PROP = "skipSslValidation"; + + public final static String ORG_GUID = "organization_guid"; + public final static String SPACE_GUID = "space_guid"; + + public static final String USERNAME_PROP = "username"; + public static final String URL_PROP = "url"; + private static final String STORE_CREDENTIALS = "storeCredentials"; + + private CFCredentials credentials; + + final private SimpleDIContext injections; + + public CFCredentials getCredentials() throws CannotAccessPropertyException { + if (credentials == null) { + StoreCredentialsMode storeMode = getStoreCredentials(); + try { + credentials = storeMode.loadCredentials(context(), type, getRunTargetId()); + } catch (Exception e) { + throw new CannotAccessPropertyException("Cannot read password.", e); + } + } + return credentials; + } + + public void setCredentials(CFCredentials credentials) throws CannotAccessPropertyException { + this.credentials = credentials; + StoreCredentialsMode storeMode = getStoreCredentials(); + storeMode.saveCredentials(context(), type, getRunTargetId(), credentials); + } + + private BootDashModelContext context() { + return injections.getBean(BootDashModelContext.class); + } + + public CloudFoundryTargetProperties(TargetProperties copyFrom, RunTargetType runTargetType, SimpleDIContext injections) { + super(copyFrom, runTargetType); + this.injections = injections; + if (get(RUN_TARGET_ID) == null) { + put(RUN_TARGET_ID, getId(this)); + } + } + + public String getSpaceName() { + return map.get(SPACE_PROP); + } + + public String getOrganizationName() { + return map.get(ORG_PROP); + } + + public String getSpaceGuid() { + return map.get(SPACE_GUID); + } + + public String getOrganizationGuid() { + return map.get(ORG_GUID); + } + + public boolean isSelfsigned() { + return map.get(SELF_SIGNED_PROP) != null && Boolean.parseBoolean(map.get(SELF_SIGNED_PROP)); + } + + public boolean skipSslValidation() { + return map.get(SKIP_SSL_VALIDATION_PROP) != null && Boolean.parseBoolean(map.get(SKIP_SSL_VALIDATION_PROP)); + } + + public static String getId(CloudFoundryTargetProperties cloudProps) { + return getId(cloudProps.getUsername(), cloudProps.getUrl(), cloudProps.getOrganizationName(), + cloudProps.getSpaceName()); + } + + public static String getName(CloudFoundryTargetProperties cloudProps) { + return cloudProps.getOrganizationName() + " : " + cloudProps.getSpaceName() + " - [" + cloudProps.getUrl() + + "]"; + } + + public static String getId(CFClientParams params) { + return getId(params.getUsername(), params.getApiUrl(), params.getOrgName(), params.getSpaceName()); + } + + public static String getId(String userName, String url, String orgName, String spaceName) { + return userName + " : " + url + " : " + orgName + " : " + spaceName; + } + + public boolean isStoreCredentials() { + return getStoreCredentials()!=StoreCredentialsMode.STORE_NOTHING; + } + + public void setSpace(CFSpace space) { + if (space != null) { + put(ORG_PROP, space.getOrganization().getName()); + put(ORG_GUID, space.getOrganization().getGuid().toString()); + put(SPACE_PROP, space.getName()); + put(SPACE_GUID, space.getGuid().toString()); + } else { + put(ORG_PROP, null); + put(ORG_GUID, null); + put(SPACE_PROP, null); + put(SPACE_GUID, null); + } + } + + public void setUrl(String value) { + put(URL_PROP, value); + } + + public void setSelfSigned(boolean value) { + put(SELF_SIGNED_PROP, Boolean.toString(value)); + } + + public void setSkipSslValidation(boolean value) { + put(SKIP_SSL_VALIDATION_PROP, Boolean.toString(value)); + } + + public String getUsername() { + return map.get(USERNAME_PROP); + } + public void setUserName(String value) { + put(USERNAME_PROP, value); + } + + public StoreCredentialsMode getStoreCredentials() { + String s = map.get(STORE_CREDENTIALS); + if (s!=null) { + return StoreCredentialsMode.valueOf(s); + } else { + return StoreCredentialsMode.STORE_NOTHING; + } + } + + public void setStoreCredentials(StoreCredentialsMode store) { + map.put(STORE_CREDENTIALS, String.valueOf(store)); + } + + public String getUrl() { + return map.get(URL_PROP); + } + + public String toJson() { + Gson gson = new Gson(); + return gson.toJson(map); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ui/CfUserInteractions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ui/CfUserInteractions.java new file mode 100644 index 000000000..5b75ec64b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ui/CfUserInteractions.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 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.cf.ui; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.OperationCanceledException; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudData; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.CustomizeAppsManagerURLDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.PasswordDialogModel; +import org.springframework.ide.eclipse.boot.dash.dialogs.ManifestDiffDialogModel; + +public interface CfUserInteractions { + void openPasswordDialog(PasswordDialogModel model); + void openEditAppsManagerURLDialog(CustomizeAppsManagerURLDialogModel model); + + /** + * Brings up the UI to enter application deployment manifest + */ + CloudApplicationDeploymentProperties promptApplicationDeploymentProperties(DeploymentPropertiesDialogModel model) throws Exception; + + ManifestDiffDialogModel.Result confirmReplaceApp(String title, CloudData cloudData, IFile manifestFile, CloudApplicationDeploymentProperties deploymentProperties) + throws OperationCanceledException, Exception; + + ManifestDiffDialogModel.Result openManifestDiffDialog(ManifestDiffDialogModel model) throws Exception; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ui/DefaultCfUserInteractions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ui/DefaultCfUserInteractions.java new file mode 100644 index 000000000..f3d050f3b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/ui/DefaultCfUserInteractions.java @@ -0,0 +1,217 @@ +package org.springframework.ide.eclipse.boot.dash.cf.ui; + +import java.lang.reflect.InvocationTargetException; + +import org.eclipse.compare.CompareConfiguration; +import org.eclipse.compare.CompareEditorInput; +import org.eclipse.compare.structuremergeviewer.DiffNode; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.text.edits.MalformedTreeException; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.TextEdit; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudData; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.ManifestDiffDialog; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.YamlGraphDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.CustomizeAppsManagerURLDialog; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.CustomizeAppsManagerURLDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialog; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.PasswordDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.UpdatePasswordDialog; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.YamlFileInput; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.YamlInput; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.dialogs.ManifestDiffDialogModel; +import org.springframework.ide.eclipse.boot.dash.dialogs.ManifestDiffDialogModel.Result; +import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.ui.UIContext; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +public class DefaultCfUserInteractions implements CfUserInteractions { + + private final SimpleDIContext context; + + public DefaultCfUserInteractions(SimpleDIContext context) { + this.context = context; + } + + private Shell getShell() { + return context.getBean(UIContext.class).getShell(); + } + + @Override + public void openPasswordDialog(PasswordDialogModel model) { + getShell().getDisplay().syncExec(new Runnable() { + @Override + public void run() { + new UpdatePasswordDialog(getShell(), model).open(); + } + }); + } + + @Override + public void openEditAppsManagerURLDialog(CustomizeAppsManagerURLDialogModel model) { + getShell().getDisplay().syncExec(new Runnable() { + @Override + public void run() { + new CustomizeAppsManagerURLDialog(model, getShell()).open(); + } + }); + } + + @Override + public CloudApplicationDeploymentProperties promptApplicationDeploymentProperties(DeploymentPropertiesDialogModel model) + throws Exception { + final Shell shell = getShell(); + + if (shell != null) { + model.initFileModel(); + model.initManualModel(); + shell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + new DeploymentPropertiesDialog(shell, model).open(); + } + }); + } + + return model.getDeploymentProperties(); + } + + @Override + public ManifestDiffDialogModel.Result confirmReplaceApp(String title, CloudData cloudData, IFile manifestFile, CloudApplicationDeploymentProperties deploymentProperties) throws Exception { + final Exception[] error = new Exception[1]; + final Result[] result = new Result[1]; + getShell().getDisplay().syncExec(() -> { + try { + result[0] = confirmReplaceApp(title, cloudData, manifestFile, deploymentProperties, new NullProgressMonitor()); + } catch (Exception e) { + error[0] = e; + } + }); + + if (error[0] != null) { + throw error[0]; + } + return result[0]; + } + + private ManifestDiffDialogModel.Result confirmReplaceApp(String title, CloudData cloudData, IFile manifestFile, CloudApplicationDeploymentProperties existingAppDeploymentProperties, IProgressMonitor monitor) throws Exception { + + + Result result = confirmReplaceAppWithManifest(title, cloudData, manifestFile, + existingAppDeploymentProperties, monitor); + if (result == null) { + String message = "Replace content of the existing Cloud application? Existing deployment properties including bound services will be retained."; + if (MessageDialog.openConfirm(getShell(), title, message)) { + // Not ideal, but using "Forget Manifest" to indicate to use existing Cloud Foundry app deployment properties + return Result.FORGET_MANIFEST; + } else { + return Result.CANCELED; + } + } else { + return result; + } + } + + private ManifestDiffDialogModel.Result confirmReplaceAppWithManifest(String title, CloudData cloudData, IFile manifestFile, CloudApplicationDeploymentProperties existingAppDeploymentProperties, IProgressMonitor monitor) throws Exception { + + if (manifestFile != null && manifestFile.isAccessible()) { + + + String yamlContents = IOUtil.toString(manifestFile.getContents()); + + YamlGraphDeploymentProperties newDeploymentProperties = new YamlGraphDeploymentProperties(yamlContents, existingAppDeploymentProperties.getAppName(), cloudData); + TextEdit edit = null; + String errorMessage = null; + try { + MultiTextEdit me = newDeploymentProperties.getDifferences(existingAppDeploymentProperties); + edit = me != null && me.hasChildren() ? me : null; + } catch (MalformedTreeException e) { + errorMessage = "Failed to create text differences between local manifest file and deployment properties on CF."; + } catch (Throwable t) { + errorMessage = "Failed to parse local manifest file YAML contents."; + } + if (errorMessage != null) { + throw ExceptionUtil.coreException(errorMessage); + } + + if (edit != 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("Existing application in 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()); + + final CompareEditorInput input = new CompareEditorInput(config) { + @Override + protected Object prepareInput(IProgressMonitor monitor) + throws InvocationTargetException, InterruptedException { + if (hasDeletedService(newDeploymentProperties, existingAppDeploymentProperties)) { + setMessage("WARNING: If using the manifest file, existing service bindings for the application that are not listed in the manifest will be removed."); + } + return new DiffNode(left, right); + } + + }; + input.setTitle("Replacing existing application"); + + input.run(monitor); + + // TODO: At the moment, this dialogue offers limited functionality to just compare manifest with existing app. Eventually + // we want to have a "full feature" compare editor that allows users to forget manifest + // or make changes + config.setLeftEditable(false); + + ManifestDiffDialogModel model = new ManifestDiffDialogModel(input); + + int val = new ManifestDiffDialog(getShell(), model, title).open(); + return ManifestDiffDialog.getResultForCode(val); + } + } + return null; + } + + private boolean hasDeletedService(YamlGraphDeploymentProperties newDeployment, CloudApplicationDeploymentProperties oldDeployment) { + return !newDeployment.getServices().containsAll(oldDeployment.getServices()); + } + + @Override + public Result openManifestDiffDialog(ManifestDiffDialogModel model) throws CoreException { + LiveVariable resultCode = new LiveVariable<>(); + LiveVariable error = new LiveVariable<>(); + getShell().getDisplay().syncExec(() -> { + try { + resultCode.setValue(new ManifestDiffDialog(getShell(), model).open()); + } catch (Exception e) { + error.setValue(e); + } + }); + if (error.getValue()!=null) { + throw ExceptionUtil.coreException(error.getValue()); + } else { + return ManifestDiffDialog.getResultForCode(resultCode.getValue()); + } + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/util/CloudErrors.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/util/CloudErrors.java new file mode 100644 index 000000000..334896672 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/util/CloudErrors.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.util; + +public class CloudErrors { + + /** + * check if access token error + * + * @param e + * @return true if access token error. False otherwise + */ + public static boolean isAccessTokenError(Exception e) { + return hasCloudError(e, "access_denied") || hasCloudError(e, "Error requesting access token") + || (hasCloudError(e, "access") && hasCloudError(e, "token")); + } + + public static boolean isBadRequest(Exception e) { + return hasCloudError(e, "400"); + } + + /** + * check 404 error. For example, application does not exist + * + * @param t + * @return true if 404 error. False otherwise + */ + public static boolean isNotFoundException(Exception e) { + return hasCloudError(e, "404"); + } + + /** + * check 503 service error. + * + * @param t + * @return true if 404 error. False otherwise + */ + public static boolean is503Error(Exception e) { + return hasCloudError(e, "503"); + } + + public static boolean hasCloudError(Exception e, String error) { + return hasError(e, error); + } + + public static void checkAndRethrowCloudException(Exception e, String errorPrefix) throws Exception { + // Special case for CF exceptions: + // CF exceptions may not contain the error in the message but rather + // the description + throw e; + } + + protected static boolean hasError(Exception exception, String pattern) { + String message = exception.getMessage(); + return message != null && message.contains(pattern); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/views/properties/CloudFoundryAppPropertiesSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/views/properties/CloudFoundryAppPropertiesSection.java new file mode 100644 index 000000000..f153dafaf --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/views/properties/CloudFoundryAppPropertiesSection.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.cf.views.properties; + +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.views.properties.AbstractBdeGeneralPropertiesSection; +import org.springframework.ide.eclipse.boot.dash.views.properties.AppPropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.BootDashElementPropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.DefaultPathPropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.InstancesPropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.ProjectPropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.ReadOnlyStringPropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.RunStatePropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.TagsPropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.UrlPropertyControl; + +/** + * Properties view section for Cloud Foundry app elements + * + * @author Alex Boyko + */ +public class CloudFoundryAppPropertiesSection extends AbstractBdeGeneralPropertiesSection { + + @Override + protected BootDashElementPropertyControl[] createPropertyControls() { + return new BootDashElementPropertyControl[] { + new RunStatePropertyControl(), + new AppPropertyControl(), + new ProjectPropertyControl(), + new InstancesPropertyControl(), + new UrlPropertyControl<>(BootDashElement.class, "URL:", ((e) -> e.getUrl())), + new DefaultPathPropertyControl(), + new ReadOnlyStringPropertyControl<>(CloudAppDashElement.class, "Healthcheck:", (e) -> e.getHealthCheck(), true), + new ReadOnlyStringPropertyControl<>(CloudAppDashElement.class, "Healthcheck Http Endpoint:", (e) -> e.getHealthCheckHttpEndpoint(), true), + new ReadOnlyStringPropertyControl<>(CloudAppDashElement.class, "Jmx Ssh Tunnel:", (e) -> e.getJmxSshTunnelStatus().getValue().getLabel(), false), + new ReadOnlyStringPropertyControl<>(CloudAppDashElement.class, "Jmx Local Port:", (e) -> e.getCfJmxPort()>0 ? (""+e.getCfJmxPort()) : "", false), + new ReadOnlyStringPropertyControl<>(CloudAppDashElement.class, "Jmx URL:", (e) -> e.getJmxUrl(), false), + new TagsPropertyControl() + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/views/properties/CloudServiceGeneralPropertiesSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/views/properties/CloudServiceGeneralPropertiesSection.java new file mode 100644 index 000000000..cd200909d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/views/properties/CloudServiceGeneralPropertiesSection.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2016 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.cf.views.properties; + +import java.util.function.Function; + +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudServiceInstanceDashElement; +import org.springframework.ide.eclipse.boot.dash.views.properties.AbstractBdeGeneralPropertiesSection; +import org.springframework.ide.eclipse.boot.dash.views.properties.AbstractBdePropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.BdeReadOnlyTextPropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.BootDashElementPropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.TagsPropertyControl; +import org.springframework.ide.eclipse.boot.dash.views.properties.UrlPropertyControl; + +/** + * Properties section for cloud service elements + * + * @author Alex Boyko + * @author Kris De Volder + */ +public class CloudServiceGeneralPropertiesSection extends AbstractBdeGeneralPropertiesSection { + + @Override + protected BootDashElementPropertyControl[] createPropertyControls() { + return new BootDashElementPropertyControl[] { + //textProperty("Name:", (e) -> e.getName()), + textProperty("Service:", (e) -> e.getService()), + textProperty("Plan:", (e) -> e.getPlan()), + textProperty("Description:", (e) -> e.getDescription()), + urlProperty("Docs:", (e) -> e.getDocumentationUrl()), + urlProperty("URL:", (e) -> e.getUrl()), + new TagsPropertyControl() + }; + } + + private AbstractBdePropertyControl urlProperty(String label, Function getter) { + return new UrlPropertyControl<>(CloudServiceInstanceDashElement.class, label, getter); + } + + private AbstractBdePropertyControl textProperty(String label, Function getter) { + return new BdeReadOnlyTextPropertyControl<>(CloudServiceInstanceDashElement.class, label, getter); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/views/properties/HealthCheckPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/views/properties/HealthCheckPropertyControl.java new file mode 100644 index 000000000..738d682bc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.cf/src/org/springframework/ide/eclipse/boot/dash/cf/views/properties/HealthCheckPropertyControl.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2015 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.cf.views.properties; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.cf.client.HealthChecks; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.ops.SetHealthCheckOperation; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.views.properties.AbstractBdePropertyControl; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; + +/** + * @author Kris De Volder + */ +public class HealthCheckPropertyControl extends AbstractBdePropertyControl { + + private CloudAppDashElement app; + private Combo selection; + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + page.getWidgetFactory().createLabel(composite, "Healthcheck:"); + selection = new Combo(composite, SWT.READ_ONLY); +// Don't use the 'CCombo' it looks really bad on linux. Doesn't properly size itself and so contents is 'chopped'. +// selection = page.getWidgetFactory().createCCombo(composite, SWT.READ_ONLY); + selection.setItems(HealthChecks.HC_ALL); + refreshControl(); + selection.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + handle(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + handle(); + } + + private void handle() { + String selected = selection.getText(); + if (StringUtil.hasText(selected)) { + SetHealthCheckOperation op = new SetHealthCheckOperation(app, selected, app.createCancelationToken()); + app.getBootDashModel().runAsynch(op, null /* no user interactions */); + } + } + }); + } + + @Override + public void refreshControl() { + if (app!=null && selection!=null && !selection.isDisposed()) { + String hc = app.getHealthCheck(); + if (StringUtil.hasText(hc)) { + selection.setText(hc); + } else { + selection.setText(""); + } + } + } + + @Override + public void setInput(BootDashElement bde) { + if (bde instanceof CloudAppDashElement) { + super.setInput(bde); + app = (CloudAppDashElement)bde; + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/.classpath new file mode 100644 index 000000000..eca7bdba8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/.project new file mode 100644 index 000000000..d9a0d1cfb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/.project @@ -0,0 +1,28 @@ + + + org.springframework.ide.eclipse.boot.dash.docker + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..9f6ece88b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/META-INF/MANIFEST.MF b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/META-INF/MANIFEST.MF new file mode 100644 index 000000000..6c4ccc5aa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/META-INF/MANIFEST.MF @@ -0,0 +1,32 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: org.springframework.ide.eclipse.boot.dash.docker +Bundle-SymbolicName: org.springframework.ide.eclipse.boot.dash.docker;singleton:=true +Bundle-Version: 4.8.1.qualifier +Automatic-Module-Name: org.springframework.ide.eclipse.boot.dash.docker +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.springframework.ide.eclipse.boot.dash, + org.springframework.ide.eclipse.boot, + org.springsource.ide.eclipse.commons.livexp, + org.eclipse.jface, + org.eclipse.ui.workbench, + org.springsource.ide.eclipse.commons.frameworks.core, + com.google.guava, + org.eclipse.core.resources, + org.yaml.snakeyaml, + org.springframework.ide.eclipse.boot.launch, + org.springsource.ide.eclipse.commons.core, + com.fasterxml.jackson.core.jackson-core, + com.fasterxml.jackson.core.jackson-databind, + org.springframework.ide.eclipse.editor.support, + org.eclipse.jdt.launching, + org.eclipse.jdt.core, + org.apache.commons.io, + org.springframework.ide.eclipse.docker.client;bundle-version="3.9.14", + org.eclipse.linuxtools.docker.ui;resolution:=optional, + org.eclipse.linuxtools.docker.core;resolution:=optional +Import-Package: org.eclipse.core.runtime, + org.eclipse.core.runtime.jobs, + org.osgi.framework +Export-Package: org.springframework.ide.eclipse.boot.dash.docker.runtarget, + org.springframework.ide.eclipse.boot.dash.docker.ui diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/build.properties b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/build.properties new file mode 100644 index 000000000..6c480f39f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/build.properties @@ -0,0 +1,6 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + plugin.xml,\ + icons/ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/README b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/README new file mode 100644 index 000000000..5306351c1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/README @@ -0,0 +1,2 @@ +The icon docker-top-logo.png was taken from the official Docker github repo and is +licensed under the Apache 2.0 license. All other icons are under the EPL 1.1 license. \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/README.md b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/README.md new file mode 100644 index 000000000..b0f05ee40 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/README.md @@ -0,0 +1,6 @@ +The icon files in here are included 'for reference' only. They are not included in plugin bundle jars. + +These are the icons from Eclipse docker tooling. + +We use some of them as is, and for others we use modified but similar variants. +Any used/modified icons are first copied into our own 'icons' folder. diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/banner-repository-context.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/banner-repository-context.gif new file mode 100644 index 000000000..f916a95d3 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/banner-repository-context.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/banner-repository.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/banner-repository.gif new file mode 100644 index 000000000..08e472ad6 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/banner-repository.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/build_exec.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/build_exec.png new file mode 100644 index 000000000..c0272bb5d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/build_exec.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/checked.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/checked.gif new file mode 100644 index 000000000..e556e7df3 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/checked.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/class_hi.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/class_hi.png new file mode 100644 index 000000000..4f3f0638f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/class_hi.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/collapseall.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/collapseall.gif new file mode 100644 index 000000000..a2d80a904 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/collapseall.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/commit.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/commit.gif new file mode 100644 index 000000000..a694ed55d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/commit.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/commitd.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/commitd.gif new file mode 100644 index 000000000..570ea0952 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/commitd.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/connection.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/connection.gif new file mode 100644 index 000000000..b5a001276 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/connection.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/console_view.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/console_view.gif new file mode 100644 index 000000000..a598f6082 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/console_view.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container.png new file mode 100644 index 000000000..85d113534 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_link.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_link.png new file mode 100644 index 000000000..cbcc0ca09 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_link.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_paused.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_paused.png new file mode 100644 index 000000000..d8a4cb263 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_paused.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_port.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_port.png new file mode 100644 index 000000000..bd86db81b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_port.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_started.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_started.png new file mode 100644 index 000000000..e3d232c06 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_started.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_stopped.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_stopped.png new file mode 100644 index 000000000..3dcf79320 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_stopped.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_volume.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_volume.png new file mode 100644 index 000000000..3d8c16c6f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/container_volume.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/context-attach.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/context-attach.gif new file mode 100644 index 000000000..bee8fc55f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/context-attach.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/copy_from_container.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/copy_from_container.gif new file mode 100644 index 000000000..7e7437bc5 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/copy_from_container.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/copy_to_container.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/copy_to_container.gif new file mode 100644 index 000000000..18576d47e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/copy_to_container.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/createcontainer.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/createcontainer.gif new file mode 100644 index 000000000..0a4455065 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/createcontainer.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/createcontainer_d.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/createcontainer_d.gif new file mode 100644 index 000000000..9681b79ec Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/createcontainer_d.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/db_obj.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/db_obj.gif new file mode 100644 index 000000000..8f2b1d589 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/db_obj.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/dbgroup_obj.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/dbgroup_obj.gif new file mode 100644 index 000000000..40d011d0b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/dbgroup_obj.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/delete.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/delete.gif new file mode 100644 index 000000000..846fb0069 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/delete.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/delete_d.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/delete_d.gif new file mode 100644 index 000000000..157b958d2 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/delete_d.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/display_log.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/display_log.gif new file mode 100644 index 000000000..55e2d04ad Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/display_log.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/docker_large.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/docker_large.png new file mode 100644 index 000000000..ce7f799d3 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/docker_large.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/docker_small.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/docker_small.gif new file mode 100644 index 000000000..b9a973da9 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/docker_small.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/environment_co.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/environment_co.gif new file mode 100644 index 000000000..716df436f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/environment_co.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/error_obj.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/error_obj.gif new file mode 100644 index 000000000..0bc60689c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/error_obj.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/file_obj.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/file_obj.gif new file mode 100644 index 000000000..efa7a3801 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/file_obj.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/filter_ps.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/filter_ps.gif new file mode 100644 index 000000000..06898144b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/filter_ps.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/folder.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/folder.gif new file mode 100644 index 000000000..03ee1dcb8 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/folder.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/folder_closed.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/folder_closed.gif new file mode 100644 index 000000000..42e027c93 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/folder_closed.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/image.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/image.gif new file mode 100644 index 000000000..555182cac Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/image.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/image.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/image.png new file mode 100644 index 000000000..f4da0ce95 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/image.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/images.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/images.png new file mode 100644 index 000000000..3092ea5e3 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/images.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/instance.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/instance.gif new file mode 100644 index 000000000..bd0e1567c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/instance.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/kill.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/kill.gif new file mode 100644 index 000000000..0bc60689c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/kill.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/killd.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/killd.gif new file mode 100644 index 000000000..cb4ee9b3b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/killd.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/labels.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/labels.gif new file mode 100644 index 000000000..f68583982 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/labels.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/links_obj.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/links_obj.gif new file mode 100644 index 000000000..6123b270e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/links_obj.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/list_UDDI_service_enabled.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/list_UDDI_service_enabled.gif new file mode 100644 index 000000000..53e1335da Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/list_UDDI_service_enabled.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/list_wsdl_highlighted.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/list_wsdl_highlighted.gif new file mode 100644 index 000000000..428c8ea5e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/list_wsdl_highlighted.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/main_tab.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/main_tab.gif new file mode 100644 index 000000000..0193dbeab Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/main_tab.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/mock-repository.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/mock-repository.gif new file mode 100644 index 000000000..1dcbaaff9 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/mock-repository.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/network_tab.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/network_tab.gif new file mode 100644 index 000000000..6cb185cff Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/network_tab.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/new_wsdl_wiz.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/new_wsdl_wiz.png new file mode 100644 index 000000000..4100110ec Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/new_wsdl_wiz.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/ports_tab.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/ports_tab.gif new file mode 100644 index 000000000..68d40f7e2 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/ports_tab.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/pull.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/pull.gif new file mode 100644 index 000000000..166eaba90 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/pull.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/push.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/push.gif new file mode 100644 index 000000000..2cb69621d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/push.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/pushd.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/pushd.gif new file mode 100644 index 000000000..2a348975b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/pushd.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/reboot.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/reboot.gif new file mode 100644 index 000000000..8da980dd1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/reboot.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/rebootd.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/rebootd.gif new file mode 100644 index 000000000..cefe52056 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/rebootd.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/refresh_tab.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/refresh_tab.gif new file mode 100644 index 000000000..3ec515bda Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/refresh_tab.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/remove_log.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/remove_log.gif new file mode 100644 index 000000000..a603027de Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/remove_log.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/remove_tag.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/remove_tag.gif new file mode 100644 index 000000000..58873a116 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/remove_tag.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repos.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repos.gif new file mode 100644 index 000000000..c13bea1ca Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repos.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repositories-blue.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repositories-blue.gif new file mode 100644 index 000000000..4ecfd3853 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repositories-blue.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repositories.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repositories.gif new file mode 100644 index 000000000..a07632446 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repositories.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-middle.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-middle.gif new file mode 100644 index 000000000..ae982990d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-middle.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-middled.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-middled.gif new file mode 100644 index 000000000..ad1ef9e30 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-middled.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-new.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-new.gif new file mode 100644 index 000000000..b4832fc94 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-new.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-synchronize-attributes.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-synchronize-attributes.png new file mode 100644 index 000000000..195659f77 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/repository-synchronize-attributes.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resolved.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resolved.gif new file mode 100644 index 000000000..8202d32d0 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resolved.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resource_obj.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resource_obj.gif new file mode 100644 index 000000000..442d6d10f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resource_obj.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/restart.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/restart.gif new file mode 100644 index 000000000..afb6fa934 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/restart.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resume.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resume.gif new file mode 100644 index 000000000..16f4e2517 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resume.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resumed.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resumed.gif new file mode 100644 index 000000000..3e130f546 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/resumed.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/run_exc.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/run_exc.png new file mode 100644 index 000000000..08571c1f2 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/run_exc.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/run_exc_d.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/run_exc_d.png new file mode 100644 index 000000000..051c86caf Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/run_exc_d.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/runlast_co.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/runlast_co.png new file mode 100644 index 000000000..0d9cdfef0 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/runlast_co.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/runlast_co_d.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/runlast_co_d.png new file mode 100644 index 000000000..efae9ba35 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/runlast_co_d.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/running.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/running.gif new file mode 100644 index 000000000..7b3a92e09 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/running.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/runningd.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/runningd.gif new file mode 100644 index 000000000..29b5aaf00 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/runningd.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/sample.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/sample.gif new file mode 100644 index 000000000..34fb3c9d8 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/sample.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/stopped.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/stopped.gif new file mode 100644 index 000000000..1543a1159 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/stopped.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/stoppedd.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/stoppedd.gif new file mode 100644 index 000000000..b2daca817 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/stoppedd.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/suspend.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/suspend.gif new file mode 100644 index 000000000..cd705c23d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/suspend.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/suspendd.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/suspendd.gif new file mode 100644 index 000000000..2c245695b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/suspendd.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/systemprocess.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/systemprocess.gif new file mode 100644 index 000000000..1ea6ec1f0 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/systemprocess.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/tag.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/tag.gif new file mode 100644 index 000000000..34027a546 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/tag.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/tag_d.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/tag_d.gif new file mode 100644 index 000000000..67f426ce9 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/tag_d.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/task-repository-new.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/task-repository-new.gif new file mode 100644 index 000000000..5f3396137 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/task-repository-new.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/task-retrieve.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/task-retrieve.gif new file mode 100644 index 000000000..ddfa476ab Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/task-retrieve.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/text_edit.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/text_edit.gif new file mode 100644 index 000000000..9312d7e0f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/text_edit.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/unchecked.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/unchecked.gif new file mode 100644 index 000000000..342fa9de7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/unchecked.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/volumes.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/volumes.gif new file mode 100644 index 000000000..c13bea1ca Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/volumes.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/warning_obj.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/warning_obj.gif new file mode 100644 index 000000000..1e5f5eb36 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/eclipse-docker-icons/warning_obj.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons-src/docker-inactive.svg b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons-src/docker-inactive.svg new file mode 100644 index 000000000..e9aa43dd0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons-src/docker-inactive.svg @@ -0,0 +1,59 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons-src/docker.svg b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons-src/docker.svg new file mode 100644 index 000000000..68cdd322e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons-src/docker.svg @@ -0,0 +1,59 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_debugging.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_debugging.png new file mode 100644 index 000000000..de483e603 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_debugging.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_debugging@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_debugging@2x.png new file mode 100644 index 000000000..101f98684 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_debugging@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_paused.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_paused.png new file mode 100644 index 000000000..d8a4cb263 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_paused.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_paused@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_paused@2x.png new file mode 100644 index 000000000..f403593ba Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_paused@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_started.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_started.png new file mode 100644 index 000000000..e3d232c06 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_started.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_started@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_started@2x.png new file mode 100644 index 000000000..db3fc6039 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_started@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_stopped.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_stopped.png new file mode 100644 index 000000000..1dee6b600 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_stopped.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_stopped@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_stopped@2x.png new file mode 100644 index 000000000..032d5aff1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/container_stopped@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker-inactive.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker-inactive.png new file mode 100644 index 000000000..634542ff8 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker-inactive.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker-inactive@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker-inactive@2x.png new file mode 100644 index 000000000..f77c7a17a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker-inactive@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker.png new file mode 100644 index 000000000..00014afd9 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker@2x.png new file mode 100644 index 000000000..949c2268e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/docker@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_debugging.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_debugging.png new file mode 100644 index 000000000..1495c28a9 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_debugging.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_debugging@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_debugging@2x.png new file mode 100644 index 000000000..a78eeef91 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_debugging@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_paused.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_paused.png new file mode 100644 index 000000000..6109dc5f6 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_paused.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_paused@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_paused@2x.png new file mode 100644 index 000000000..590224b50 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_paused@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_started.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_started.png new file mode 100644 index 000000000..cbdb9fa32 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_started.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_started@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_started@2x.png new file mode 100644 index 000000000..b3d7273da Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_started@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_stopped.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_stopped.png new file mode 100644 index 000000000..d0763e91e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/icons/image_stopped.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/plugin.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/plugin.xml new file mode 100644 index 000000000..9d0dbc01a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/plugin.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/pom.xml new file mode 100755 index 000000000..3a232285d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + org.springframework.boot.ide + org.springframework.boot.ide + 4.8.1-SNAPSHOT + ../../eclipse-distribution/pom.xml + + + org.springframework.ide.eclipse + org.springframework.ide.eclipse.boot.dash.docker + eclipse-plugin + + + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + second-generate-p2-metadata + + p2-metadata + + verify + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/readme.md b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/readme.md new file mode 100644 index 000000000..0cad50e69 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/readme.md @@ -0,0 +1 @@ +https://docs.google.com/document/d/1toXVPADKoGinlsxj0vjNGTgZwWmkQa4dH_0nadgggxo/edit?usp=sharing diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/DockerBootDashInjections.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/DockerBootDashInjections.java new file mode 100644 index 000000000..6dffeb904 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/DockerBootDashInjections.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2020 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.docker; + +import org.springframework.ide.eclipse.boot.dash.di.EclipseBeanLoader.Contribution; +import org.springframework.ide.eclipse.boot.dash.docker.runtarget.DockerRunTargetType; +import org.springframework.ide.eclipse.boot.dash.docker.ui.DefaultDockerUserInteractions; +import org.springframework.ide.eclipse.boot.dash.docker.ui.DockerUserInteractions; +import org.springframework.ide.eclipse.boot.dash.docker.ui.ExtraDockerActions; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; + +public class DockerBootDashInjections implements Contribution { + + @Override + public void applyBeanDefinitions(SimpleDIContext c) throws Exception { + c.def(DockerRunTargetType.class, DockerRunTargetType::new); + c.def(DockerUserInteractions.class, DefaultDockerUserInteractions::new); + c.defInstance(ExtraDockerActions.class, new ExtraDockerActions()); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/actions/OpenDockerExplorerAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/actions/OpenDockerExplorerAction.java new file mode 100644 index 000000000..cfb48a567 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/actions/OpenDockerExplorerAction.java @@ -0,0 +1,89 @@ +package org.springframework.ide.eclipse.boot.dash.docker.actions; + +import org.eclipse.linuxtools.docker.core.DockerConnectionManager; +import org.eclipse.linuxtools.internal.docker.core.DockerConnection; +import org.eclipse.linuxtools.internal.docker.core.DockerConnection.Builder; +import org.eclipse.linuxtools.internal.docker.core.TCPConnectionSettings; +import org.eclipse.linuxtools.internal.docker.core.UnixSocketConnectionSettings; +import org.eclipse.linuxtools.internal.docker.ui.views.DockerExplorerView; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.docker.runtarget.DockerRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashModelAction; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +@SuppressWarnings("restriction") +public class OpenDockerExplorerAction extends AbstractBootDashModelAction { + + @SuppressWarnings("restriction") + public OpenDockerExplorerAction(LiveExpression section, SimpleDIContext context) { + super(section, context); + this.setText("Open Docker Explorer"); + this.setToolTipText("Open Eclipse's Docker Explorer View"); +// this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/remove_target.png")); +// this.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/remove_target_disabled.png")); + } + + private boolean shouldEnable() { + return getRunTarget() instanceof DockerRunTarget; + } + + public void updateEnablement() { + this.setEnabled(shouldEnable()); + } + + public void updateVisibility() { + this.setVisible(shouldEnable()); + } + + private static final String VIEW_TYPE_ID = DockerExplorerView.VIEW_ID; + + @Override + public void run() { + DockerRunTarget target = getRunTarget(); + if (target!=null) { + try { + DockerConnectionManager connections = DockerConnectionManager.getInstance(); + if (connections.getAllConnections().isEmpty()) { + Builder cb = new DockerConnection.Builder() + .name(target.getName()); + DockerConnection c = null; + String uri = target.getParams().getUri(); + if (uri.startsWith("unix:")) { + c = cb.unixSocketConnection(new UnixSocketConnectionSettings(uri)); + } else if (uri.startsWith("tcp:")) { + String host = uri.substring("tcp:".length()); + while (host.startsWith("/")) { + host = host.substring(1); + } + c = cb.tcpConnection(new TCPConnectionSettings(host, null)); + } + connections.addConnection(c); + } + + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + if (page!=null) { + page.showView(VIEW_TYPE_ID); + } + } catch (Exception e) { + Log.log(e); + } + } + } + + private DockerRunTarget getRunTarget() { + BootDashModel section = sectionSelection.getValue(); + if (section != null) { + RunTarget t = section.getRunTarget(); + if (t instanceof DockerRunTarget) { + return (DockerRunTarget) t; + } + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/jmx/JmxSupport.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/jmx/JmxSupport.java new file mode 100644 index 000000000..193c22672 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/jmx/JmxSupport.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2018,2020 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.docker.jmx; + +import java.util.EnumSet; +import java.util.Map; + +import org.springframework.ide.eclipse.boot.launch.livebean.JmxBeanSupport; +import org.springframework.ide.eclipse.boot.launch.util.PortFinder; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import static org.springframework.ide.eclipse.boot.launch.livebean.JmxBeanSupport.Feature.*; + +/** + * Helper class providing functionality to connect to JMX on a remote spring boot app + * running on CF, using ssh tunneling. + *

+ * The main responsiblity of this class is to manage tunnel life-cycle based on the + * app's state. (I.e. ensure tunnel is created when app started and tunnel is closed + * when app stopped or deleted). + * + * @author Kris De Volder + */ +public class JmxSupport { + + private static final String JAVA_OPTS = "JAVA_OPTS"; + + private static final String JMX_OPTION_PAT = + "-D(com\\.sun\\.management\\.jmxremote|java\\.rmi\\.server|spring\\.jmx)\\.[a-z\\.]*=\\S*\\s*"; + + private static final String JMX_ARGS(int port) { +// return JmxBeanSupport.jmxBeanVmArgs(port, EnumSet.of(JMX, LIFE_CYCLE)); + return "-Dcom.sun.management.jmxremote.ssl=false " + + "-Dcom.sun.management.jmxremote.authenticate=false " + + "-Dcom.sun.management.jmxremote.port="+port+" " + + "-Dcom.sun.management.jmxremote.rmi.port="+port+" " + + "-Djava.rmi.server.hostname=localhost " + + "-Dcom.sun.management.jmxremote.local.only=false "+ + "-Dspring.jmx.enabled=true " + + "-Dspring.application.admin.enabled=true"; + } + + int port; + + + public JmxSupport(int port) { + this.port = port; + } + + public JmxSupport() { + try { + port = PortFinder.findFreePort(); + } catch (Exception e) { + Log.log(e); + port = 0; + } + } + + public void setupEnvVars(int port, Map env) { + if (port>0) { + String javaOpts = env.get(JAVA_OPTS); + if (javaOpts!=null) { + //Erase old vars + javaOpts = javaOpts.replaceAll(JMX_OPTION_PAT, "").trim(); + } else { + javaOpts = ""; + } + String jmxArgs = JMX_ARGS(port); + if ("".equals(javaOpts)) { + // no other java opts yet + javaOpts = jmxArgs; + } else { + javaOpts = javaOpts + " " +jmxArgs; + } + env.put(JAVA_OPTS, javaOpts); + } + } + + public String getJmxUrl() { + if (port>0) { + return "service:jmx:rmi://localhost:"+port+"/jndi/rmi://localhost:"+port+"/jmxrmi"; + } else { + return null; + } + } + + public int getPort() { + return port; + } + + public String getJavaOpts() { + return JMX_ARGS(port); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerApp.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerApp.java new file mode 100644 index 000000000..8d9133c75 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerApp.java @@ -0,0 +1,593 @@ +/******************************************************************************* + * Copyright (c) 2020 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.docker.runtarget; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.io.FileUtils; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.launching.IVMInstall; +import org.eclipse.jdt.launching.JavaRuntime; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.AppConsole; +import org.springframework.ide.eclipse.boot.dash.api.AppConsoleProvider; +import org.springframework.ide.eclipse.boot.dash.api.AppContext; +import org.springframework.ide.eclipse.boot.dash.api.Deletable; +import org.springframework.ide.eclipse.boot.dash.api.DesiredInstanceCount; +import org.springframework.ide.eclipse.boot.dash.api.DevtoolsConnectable; +import org.springframework.ide.eclipse.boot.dash.api.LogConnection; +import org.springframework.ide.eclipse.boot.dash.api.LogSource; +import org.springframework.ide.eclipse.boot.dash.api.ProjectRelatable; +import org.springframework.ide.eclipse.boot.dash.api.SystemPropertySupport; +import org.springframework.ide.eclipse.boot.dash.api.TemporalBoolean; +import org.springframework.ide.eclipse.boot.dash.console.LogType; +import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsUtil; +import org.springframework.ide.eclipse.boot.dash.docker.jmx.JmxSupport; +import org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.remote.ChildBearing; +import org.springframework.ide.eclipse.boot.dash.model.remote.RefreshStateTracker; +import org.springframework.ide.eclipse.boot.dash.util.LineBasedStreamGobler; +import org.springframework.ide.eclipse.boot.launch.util.PortFinder; +import org.springframework.ide.eclipse.boot.util.JavaProjectUtil; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.core.util.OsUtils; +import org.springsource.ide.eclipse.commons.frameworks.core.util.JobUtil; +import org.springsource.ide.eclipse.commons.frameworks.core.util.StringUtils; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +import org.springsource.ide.eclipse.commons.livexp.util.Log; +import org.springsource.ide.eclipse.commons.livexp.util.OldValueDisposer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.exception.NotModifiedException; +import com.github.dockerjava.api.model.Container; +import com.github.dockerjava.api.model.ExposedPort; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Image; +import com.github.dockerjava.api.model.Network; +import com.github.dockerjava.api.model.PortBinding; +import com.github.dockerjava.api.model.Ports.Binding; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +public class DockerApp extends AbstractDisposable implements App, ChildBearing, Deletable, ProjectRelatable, DesiredInstanceCount, SystemPropertySupport, LogSource, DevtoolsConnectable { + + + + private static final String DOCKER_IO_LIBRARY = "docker.io/library/"; + private static final String[] NO_STRINGS = new String[0]; + private DockerClient client; + private final IProject project; + private DockerRunTarget target; + private final String name; + + public static final String APP_NAME = "sts.app.name"; + public static final String BUILD_ID = "sts.app.build-id"; + public static final String SYSTEM_PROPS = "sts.app.sysprops"; + public static final String JMX_PORT = "sts.app.jmx.port"; + public static final String DEBUG_PORT = "sts.app.debug.port"; + public static final String APP_LOCAL_PORT = "sts.app.port.local"; + + private static final int STOP_WAIT_TIME_IN_SECONDS = 20; + public final CompletableFuture refreshTracker = new CompletableFuture<>(); + + private OldValueDisposer containerLogConnection = new OldValueDisposer<>(this); + private AppContext context; + + private static File initFile; + + private final String DEBUG_JVM_ARGS(String debugPort) { + if (JavaProjectUtil.isJava9OrLater(project)) { + return "-Xdebug -Xrunjdwp:server=y,transport=dt_socket,suspend=n,address=*:"+debugPort; + } else { + return "-Xdebug -Xrunjdwp:server=y,transport=dt_socket,suspend=n,address="+debugPort; + } + } + + public DockerApp(String name, DockerRunTarget target, DockerClient client) { + this.target = target; + this.name = name; + this.client = client; + this.project = ResourcesPlugin.getWorkspace().getRoot().getProject(deployment().getName()); + } + + public DockerClient getClient() { + return this.client; + } + + public DockerDeployment deployment() { + return target.deployments.get(name); + } + + @Override + public EnumSet supportedGoalStates() { + return EnumSet.of(RunState.INACTIVE, RunState.RUNNING, RunState.DEBUGGING); + } + + @Override + public String getName() { + return name; + } + + @Override + public List fetchChildren() throws Exception { + Builder builder = ImmutableList.builder(); + if (client!=null) { + List images = JobUtil.interruptAfter(Duration.ofSeconds(15), + () -> client.listImagesCmd().withShowAll(true).exec() + ); + synchronized (this) { + Set persistedImages = new HashSet<>(Arrays.asList(getPersistedImages())); + Set existingImages = new HashSet<>(); + for (Image image : images) { + if (persistedImages.contains(image.getId())) { + builder.add(new DockerImage(this, image)); + existingImages.add(image.getId()); + } + } + setPersistedImages(existingImages); + } + } + return builder.build(); + } + + @Override + public void delete() throws Exception { + target.deployments.remove(project.getName()); + } + + @Override + public DockerRunTarget getTarget() { + return this.target; + } + + public CompletableFuture synchronizeWithDeployment() { + return this.refreshTracker.thenComposeAsync(refreshTracker -> { + DockerDeployment deployment = deployment(); + return refreshTracker.runAsync("Synchronizing deployment "+deployment.getName(), () -> { + String currentSession = this.target.sessionId.getValue(); + if (currentSession.equals(deployment.getSessionId())) { + RunState desiredRunState = deployment.getRunState(); + List containers = client.listContainersCmd() + .withShowAll(true) + .withLabelFilter(ImmutableMap.of(APP_NAME, getName())) + .exec(); + if (desiredRunState==RunState.INACTIVE) { + stop(containers); + } else if (desiredRunState.isActive()) { + List toStop = new ArrayList<>(containers.size()); + boolean desiredContainerFound = false; + for (Container c : containers) { + if (isMatchingContainer(deployment, c)) { + if (new DockerContainer(getTarget(), this, c).fetchRunState()==desiredRunState) { + desiredContainerFound = true; + } + } else { + toStop.add(c); + } + } + stop(toStop); + if (!desiredContainerFound) { + start(deployment); + } + } + } + }); + }); + } + + /** + * Checks whether a container's metadata matches all of the desired deployment props. + * But without considering runstate. + */ + private boolean isMatchingContainer(DockerDeployment d, Container c) { + String desiredBuildId = d.getBuildId(); + Map desiredProps = d.getSystemProperties(); + return desiredBuildId.equals(c.getLabels().get(BUILD_ID)) && + desiredProps.equals(DockerContainer.getSystemProps(c)); + } + + private void stop(List containers) throws Exception { + RefreshStateTracker refreshTracker = this.refreshTracker.get(); + refreshTracker.run("Stopping containers for app "+name, () -> { + for (Container container : containers) { + try { + client.stopContainerCmd(container.getId()).withTimeout(STOP_WAIT_TIME_IN_SECONDS).exec(); + } catch (NotModifiedException e) { + //ignore... this isn't a real error, it means container was already stopped. + } + } + }); + } + + public void start(DockerDeployment deployment) throws Exception { + RefreshStateTracker refreshTracker = this.refreshTracker.get(); + + AppConsole console = target.injections().getBean(AppConsoleProvider.class).getConsole(this); + console.show(); + + String image = refreshTracker.call("Building image" + BootDashLabels.ELLIPSIS, () -> { + if (!project.isAccessible()) { + throw new IllegalStateException("The project '"+project.getName()+"' is not accessible"); + } + console.write("Deploying Docker app " + getName() + BootDashLabels.ELLIPSIS, LogType.STDOUT); + return build(console); + }); + + refreshTracker.run("Starting container '" + image + "'" + + BootDashLabels.ELLIPSIS, () -> { + run(console, image, deployment); + console.write("DONE Deploying Docker app " + getName(), LogType.STDOUT); + }); + } + + private void run(AppConsole console, String image, DockerDeployment deployment) throws Exception { + if (client==null) { + console.write("Cannot start container... Docker client is disconnected!", LogType.STDERROR); + } else { + Network network = target.ensureNetwork(); + console.write("Running container with '"+image+"'", LogType.STDOUT); + JmxSupport jmx = new JmxSupport(); + String jmxUrl = jmx.getJmxUrl(); + if (jmxUrl!=null) { + console.write("JMX URL = "+jmxUrl, LogType.STDOUT); + } + String desiredBuildId = deployment.getBuildId(); + Map systemProperties = deployment.getSystemProperties(); + String sysprops = new ObjectMapper().writeValueAsString(systemProperties); + ImmutableMap.Builder labels = ImmutableMap.builder() + .put(APP_NAME, getName()) + .put(BUILD_ID, desiredBuildId) + .put(SYSTEM_PROPS, sysprops); + + ImmutableSet.Builder exposedPorts = ImmutableSet.builder(); + ImmutableList.Builder portBindings = ImmutableList.builder(); + + CreateContainerCmd cb = client.createContainerCmd(image); +// cb.withHostName(getName()); +// cb.withNetworkMode("bridge"); +// cb.withAliases(getName()); + + int appLocalPort = PortFinder.findFreePort(); + int appContainerPort = 8080; + + if (appLocalPort > 0) { + labels.put(APP_LOCAL_PORT, ""+appLocalPort); + portBindings.add(new PortBinding(new Binding("0.0.0.0", ""+appLocalPort), ExposedPort.tcp(appContainerPort))); + exposedPorts.add(ExposedPort.tcp(appContainerPort)); + } + + StringBuilder javaOpts = new StringBuilder(); + + if (jmxUrl!=null) { + int jmxPort = jmx.getPort(); + labels.put(JMX_PORT, ""+jmxPort); + + portBindings.add(new PortBinding(new Binding("0.0.0.0", ""+jmxPort), ExposedPort.tcp(jmxPort))); + exposedPorts.add(ExposedPort.tcp(jmxPort)); + + javaOpts.append(jmx.getJavaOpts()); + javaOpts.append(" "); + } + + RunState desiredRunState = deployment.getRunState(); + if (desiredRunState==RunState.DEBUGGING) { + int debugPort = PortFinder.findFreePort(); + labels.put(DockerApp.DEBUG_PORT, ""+debugPort); + + portBindings.add(new PortBinding(new Binding("0.0.0.0", ""+debugPort), ExposedPort.tcp(debugPort))); + exposedPorts.add(ExposedPort.tcp(debugPort)); + + javaOpts.append(DEBUG_JVM_ARGS(""+debugPort)); + console.write("Debug Port = "+debugPort, LogType.STDOUT); + javaOpts.append(" "); + } + + if (!systemProperties.isEmpty()) { + for (Entry prop : systemProperties.entrySet()) { + Assert.isTrue(!prop.getValue().contains(" ")); //TODO: Escaping stuff like spaces in the value + Assert.isTrue(!prop.getValue().contains("\t")); + Assert.isTrue(!prop.getValue().contains("\n")); + Assert.isTrue(!prop.getValue().contains("\r")); + javaOpts.append("-D"+prop.getKey()+"="+prop.getValue()); + javaOpts.append(" "); + } + } + + String javaOptsStr = javaOpts.toString(); + if (StringUtils.hasText(javaOptsStr)) { + cb.withEnv("JAVA_OPTS="+javaOptsStr.trim()); + console.write("JAVA_OPTS="+javaOptsStr.trim(), LogType.STDOUT); + } + + cb.withHostConfig(new HostConfig() + .withPortBindings(portBindings.build()) + .withNetworkMode(network.getName()) + ); + cb.withExposedPorts(exposedPorts.build().asList()); + String networkAlias = getName(); + cb.withAliases(networkAlias); + cb.withLabels(labels.build()); + CreateContainerResponse c = cb.exec(); + console.write("Container created: "+c.getId(), LogType.STDOUT); + console.write("Starting container: "+c.getId(), LogType.STDOUT); + console.write("Ports: "+appLocalPort+"->"+appContainerPort, LogType.STDOUT); + console.write("Container Network: "+ network.getName(), LogType.STDOUT); + console.write("Network alias: "+networkAlias , LogType.STDOUT); + + //Disabled show of console here. See: https://www.pivotaltracker.com/story/show/174316849 + //appContext.showConsole(c.id()); + + client.startContainerCmd(c.getId()).exec(); + containerLogConnection.setValue(DockerContainer.connectLog(target, c.getId(), console, true)); + } + } + + private static final Pattern BUILT_IMAGE_MESSAGE = Pattern.compile("Successfully built image.*\\'(.*)\\'"); +// private static final Pattern BUILT_IMAGE_MESSAGE = Pattern.compile("Successfully built image"); + + private String build(AppConsole console) throws Exception { + AtomicReference image = new AtomicReference<>(); + File directory = new File(project.getLocation().toString()); + String[] command = getBuildCommand(directory); + + ProcessBuilder builder = new ProcessBuilder(command).directory(directory); + builder.environment().put("JAVA_HOME", getJavaHome()); + + Process process = builder.start(); + LineBasedStreamGobler outputGobler = new LineBasedStreamGobler(process.getInputStream(), (line) -> { + System.out.println(line); + Matcher matcher = BUILT_IMAGE_MESSAGE.matcher(line); + if (matcher.find()) { + image.set(matcher.group(1)); + } + try { + console.write(line, LogType.APP_OUT); + } catch (Exception e) { + Log.log(e); + } + }); + new LineBasedStreamGobler(process.getErrorStream(), (line) -> { + try { + console.write(line, LogType.APP_ERROR); + } catch (Exception e) { + Log.log(e); + } + }); + int exitCode = process.waitFor(); + if (exitCode!=0) { + throw new IOException("Command execution failed!"); + } + outputGobler.join(); + + String imageTag = image.get(); + if (imageTag.startsWith(DOCKER_IO_LIBRARY)) { + imageTag = imageTag.substring(DOCKER_IO_LIBRARY.length()); + } + List images = client.listImagesCmd().withImageNameFilter(imageTag).exec(); + + for (Image img : images) { + addPersistedImage(img.getId()); + } + + return imageTag; + } + + private String getJavaHome() throws CoreException { + IVMInstall jvm = JavaRuntime.getVMInstall(JavaCore.create(project)); + return jvm.getInstallLocation().toString(); + } + + private String[] getBuildCommand(File directory) { + boolean isMaven = true; + List command = new ArrayList<>(); + if (OsUtils.isWindows()) { + if (Files.exists(directory.toPath().resolve("mvnw.cmd"))) { + command.addAll(ImmutableList.of("CMD", "/C", "mvnw.cmd", "spring-boot:build-image", "-DskipTests")); + //, "-Dspring-boot.repackage.excludeDevtools=false" }; + } else if (Files.exists(directory.toPath().resolve("gradlew.bat"))) { + isMaven = false; + command.addAll(ImmutableList.of("CMD", "/C", "gradlew.bat", "bootBuildImage", "-x", "test")); + } + } else { + if (Files.exists(directory.toPath().resolve("sts-docker-build.sh"))) { + command.addAll(ImmutableList.of("./sts-docker-build.sh")); + } else if (Files.exists(directory.toPath().resolve("mvnw"))) { + command.addAll(ImmutableList.of("./mvnw", "spring-boot:build-image", "-DskipTests")); + } else if (Files.exists(directory.toPath().resolve("gradlew"))) { + isMaven = false; + command.addAll(ImmutableList.of("./gradlew", "--stacktrace", "bootBuildImage", "-x", "test" )); + } + } + if (command.isEmpty()) { + throw new IllegalStateException("Neither sts-docker-build.sh nor Gradle/Maven wrapper was found!"); + } + boolean wantsDevtools = deployment().getSystemProperties().getOrDefault(DevtoolsUtil.REMOTE_SECRET_PROP, null)!=null; + if (wantsDevtools) { + if (isMaven) { + command.add("-Dspring-boot.repackage.excludeDevtools=false"); + } else { + try { + command.addAll(gradle_initScript( + "allprojects {\n" + + " afterEvaluate {\n" + + " bootJar {\n" + + " classpath configurations.developmentOnly\n" + + " }\n" + + " }\n" + + "}" + )); + } catch (Exception e) { + Log.log(e); + } + } + } + return command.toArray(new String[command.size()]); + } + + private synchronized static List gradle_initScript(String script) throws IOException { + System.out.println(script); + if (initFile==null) { + initFile = File.createTempFile("init-script", ".gradle"); + FileUtils.writeStringToFile(initFile, script, "UTF8"); + initFile.deleteOnExit(); + } + return ImmutableList.of( + "-I", + initFile.getAbsolutePath() + ); + } + + synchronized private void addPersistedImage(String imageId) { + String key = imagesKey(); + try { + ImmutableSet.Builder builder = ImmutableSet.builder(); + PropertyStoreApi props = getTarget().getPersistentProperties(); + builder.addAll(Arrays.asList(props.get(key, NO_STRINGS))); + builder.add(imageId); + props.put(key, builder.build().toArray(NO_STRINGS)); + + } catch (Exception e) { + Log.log(e); + } + } + + private void setPersistedImages(Set existingImages) { + try { + getTarget().getPersistentProperties().put(imagesKey(), existingImages.toArray(NO_STRINGS)); + } catch (Exception e) { + Log.log(e); + } + } + + private String[] getPersistedImages() { + try { + return getTarget().getPersistentProperties().get(imagesKey(), NO_STRINGS); + } catch (Exception e) { + Log.log(e); + } + return NO_STRINGS; + } + + private String imagesKey() { + return getName() + ".images"; + } + + @Override + public void setContext(AppContext context) { + this.refreshTracker.complete(context.getRefreshTracker()); + this.context = context; + } + + @Override + public void restart(RunState runningOrDebugging) { + DockerDeployment d = deployment(); + d.setBuildId(UUID.randomUUID().toString()); + d.setSessionId(target.sessionId.getValue()); + d.setRunState(runningOrDebugging); + target.deployments.createOrUpdate(d); + } + + @Override + public void setSystemProperty(String name, String value) { + DockerDeployment d = new DockerDeployment(deployment()); + d.setSystemProperty(name, value); + d.setSessionId(target.sessionId.getValue()); + target.deployments.createOrUpdate(d); + } + + @Override + public String getSystemProperty(String key) { + DockerDeployment d = deployment(); + if (d!=null ) { + return d.getSystemProperties().getOrDefault(key, null); + } + return null; + } + + @Override + public void setGoalState(RunState newGoalState) { + DockerDeployment deployment = deployment(); + if (deployment.getRunState()!=newGoalState) { + target.deployments.createOrUpdate(deployment.withGoalState(newGoalState, target.sessionId.getValue())); + } + } + + @Override + public IProject getProject() { + return project; + } + + @Override + public int getDesiredInstances() { + DockerDeployment deployment = deployment(); + if (deployment != null) { + return deployment.getDesiredInstances(); + } + return 0; + } + + + @Override + public String getConsoleDisplayName() { + return getName() + " - image build output @ "+getTarget().getName(); + } + + @Override + public LogConnection connectLog(AppConsole logConsole, boolean includeHistory) { + // There is nothing to connect to because docker app only writes output directly to the console + // There is no streaming API to fetch output. + return null; + } + + @Override + public TemporalBoolean isDevtoolsConnectable() { + return TemporalBoolean.NEVER; + } + + @Override + public String getDevtoolsSecret() { + DockerDeployment d = deployment(); + if (d!=null) { + return d.getSystemProperties().getOrDefault(DevtoolsUtil.REMOTE_SECRET_PROP, null); + } + return null; + } + + @Override + public boolean hasDevtoolsDependency() { + return context!=null && context.projectHasDevtoolsDependency(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerContainer.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerContainer.java new file mode 100644 index 000000000..22242e322 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerContainer.java @@ -0,0 +1,564 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.docker.runtarget; + +import static org.eclipse.ui.plugin.AbstractUIPlugin.imageDescriptorFromPlugin; +import static org.springframework.ide.eclipse.boot.dash.docker.runtarget.DockerRunTargetType.PLUGIN_ID; + +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; +import java.time.Duration; +import java.time.Instant; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.StyledString; +import org.springframework.ide.eclipse.boot.dash.api.ActualInstanceCount; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.AppConsole; +import org.springframework.ide.eclipse.boot.dash.api.AppConsoleProvider; +import org.springframework.ide.eclipse.boot.dash.api.AppContext; +import org.springframework.ide.eclipse.boot.dash.api.DebuggableApp; +import org.springframework.ide.eclipse.boot.dash.api.Deletable; +import org.springframework.ide.eclipse.boot.dash.api.DevtoolsConnectable; +import org.springframework.ide.eclipse.boot.dash.api.JmxConnectable; +import org.springframework.ide.eclipse.boot.dash.api.LogConnection; +import org.springframework.ide.eclipse.boot.dash.api.LogSource; +import org.springframework.ide.eclipse.boot.dash.api.PortConnectable; +import org.springframework.ide.eclipse.boot.dash.api.ProjectRelatable; +import org.springframework.ide.eclipse.boot.dash.api.RunStateIconProvider; +import org.springframework.ide.eclipse.boot.dash.api.RunStateProvider; +import org.springframework.ide.eclipse.boot.dash.api.Styleable; +import org.springframework.ide.eclipse.boot.dash.console.LogType; +import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsUtil; +import org.springframework.ide.eclipse.boot.dash.docker.jmx.JmxSupport; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.remote.RefreshStateTracker; +import org.springframework.ide.eclipse.boot.util.RetryUtil; +import org.springframework.ide.eclipse.editor.support.yaml.path.JavaObjectNav; +import org.springframework.ide.eclipse.editor.support.yaml.path.YamlNavigable; +import org.springframework.ide.eclipse.editor.support.yaml.path.YamlPath; +import org.springframework.ide.eclipse.editor.support.yaml.path.YamlTraversal; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.frameworks.core.util.StringUtils; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.async.ResultCallback; +import com.github.dockerjava.api.command.LogContainerCmd; +import com.github.dockerjava.api.exception.NotFoundException; +import com.github.dockerjava.api.model.Container; +import com.github.dockerjava.api.model.Frame; +import com.github.dockerjava.api.model.StreamType; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +public class DockerContainer implements App, RunStateProvider, JmxConnectable, Styleable, PortConnectable, + Deletable, ActualInstanceCount, DebuggableApp, ProjectRelatable, DevtoolsConnectable, LogSource, RunStateIconProvider +{ + + public static final Duration WAIT_BEFORE_KILLING = Duration.ofSeconds(10); + private static final boolean DEBUG = false; + private final Container container; + private final DockerRunTarget target; + public final CompletableFuture refreshTracker = new CompletableFuture<>(); + + private static Map RUNSTATE_ICONS = null; + private DockerApp app; + + public DockerContainer(DockerRunTarget target, DockerApp app, Container container) { + this.target = target; + this.app = app; + this.container = container; + this.hasDevtoolsDep = hasDevtoolsDependency(container::getLabels); + } + + public static Supplier hasDevtoolsDependency(Supplier> labelsSupplier) { + return Suppliers.memoize(() ->{ + Map labels = labelsSupplier.get(); + try { + Map bpmd = new ObjectMapper().readValue(labels.get("io.buildpacks.build.metadata"), Map.class); + Set deps = dependencyNamePath + .traverseAmbiguously(YamlNavigable.javaObject(bpmd)) + .flatMap(JavaObjectNav::asStringMaybe).collect(Collectors.toSet()); + return deps.contains("spring-boot-devtools"); + } catch (Exception e) { + Log.log(e); + } + return false; + }); + } + + @Override + public String getName() { + return container.getId(); + } + + @Override + public RunState fetchRunState() { + return getRunState(container); + } + + public static RunState getRunState(Container container) { + String state = container.getState(); + if ("running".equals(state)) { + return (container.getLabels().get(DockerApp.DEBUG_PORT)!=null) + ? RunState.DEBUGGING + : RunState.RUNNING; + } else if ("exited".equals(state)) { + return RunState.INACTIVE; + } else if ("paused".equals(state)) { + return RunState.PAUSED; + } else if ("created".equals(state)) { + return RunState.STARTING; + } + return RunState.UNKNOWN; + } + + @Override + public DockerRunTarget getTarget() { + return this.target; + } + + @Override + public String getJmxUrl() { + try { + String port = container.getLabels().get(DockerApp.JMX_PORT); + if (port!=null) { + return new JmxSupport(Integer.valueOf(port)).getJmxUrl(); + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + @Override + public String toString() { + return "DockerContainer("+container.getId()+")"; + } + + + @Override + public StyledString getStyledName(Stylers stylers) { + StyledString styledString = new StyledString(); + if (container.getNames() != null && container.getNames().length>0) { + styledString = styledString.append(StringUtil.removePrefix(container.getNames()[0], "/")); + } + styledString = styledString.append(" (" +getShortHash()+")", StyledString.QUALIFIER_STYLER); + return styledString; + } + + private String getShortHash() { + String id = container.getId(); + if (id.length() > 12) { + id = id.substring(0, 12); + } + return id; + } + + @Override + public Set getPorts() { + ImmutableSet.Builder livePorts = ImmutableSet.builder(); + String portVal = container.getLabels().get(DockerApp.APP_LOCAL_PORT); + if (portVal != null) { + livePorts.add(Integer.parseInt(portVal)); + } + return livePorts.build(); + } + + @Override + public EnumSet supportedGoalStates() { + Set supported = new HashSet<>(); + supported.add(RunState.INACTIVE); + if (container.getLabels().get(DockerApp.DEBUG_PORT)!=null) { + supported.add(RunState.DEBUGGING); + } else { + supported.add(RunState.RUNNING); + } + supported.add(RunState.PAUSED); + return EnumSet.copyOf(supported); + } + + @Override + public void setGoalState(RunState goal) { + RunState currentState = fetchRunState(); + if (currentState != goal) { + + DockerRunTarget dockerTarget = getTarget(); + DockerClient client = dockerTarget.getClient(); + if (client != null) { + try { + RefreshStateTracker rt = this.refreshTracker.get(); + + if (goal.isActive()) { + if (currentState==RunState.PAUSED) { + rt.run("Resuming " + getStyledName(null).getString(), () -> { + client.unpauseContainerCmd(container.getId()).exec(); + RetryUtil.until(100, 1000, runstate -> runstate.isActive(), this::fetchRunState); + }); + } else { + rt.run("Starting " + getStyledName(null).getString(), () -> { + client.startContainerCmd(container.getId()).exec(); + RetryUtil.until(100, 1000, runstate -> runstate.equals(RunState.RUNNING), this::fetchRunState); + }); + } + } else if (goal == RunState.INACTIVE) { + rt.run("Stopping " + getShortHash(), () -> { + debug("Stopping "); + client.stopContainerCmd(container.getId()).withTimeout((int) WAIT_BEFORE_KILLING.getSeconds()).exec(); + debug("Waiting for stopped state..."); + RetryUtil.until(100, WAIT_BEFORE_KILLING.toMillis(), + runstate -> runstate.equals(RunState.INACTIVE), this::fetchRunState); + debug("Stopped "); + }); + } else if (goal == RunState.PAUSED) { + rt.run("Suspending "+getStyledName(null).getString(), () -> { + client.pauseContainerCmd(getName()).exec(); + }); + RetryUtil.until(100, WAIT_BEFORE_KILLING.toMillis(), + runstate -> runstate.equals(RunState.PAUSED), this::fetchRunState); + } + } catch (Exception e) { + Log.log(e); + } + } + } + } + + private void debug(String message) { + if (DEBUG) { + System.out.println("DockerContainer " + getShortHash() + ": " + message); + } + } + + @Override + public void restart(RunState runingOrDebugging) { + DockerRunTarget dockerTarget = getTarget(); + DockerClient client = dockerTarget.getClient(); + if (client != null) { + try { + AppConsole console = target.injections().getBean(AppConsoleProvider.class).getConsole(this); + console.show(); + + RefreshStateTracker rt = this.refreshTracker.get(); + rt.run("Starting " + getShortHash(), () -> { + client.restartContainerCmd(container.getId()).exec(); + RetryUtil.until(100, 1000, runstate -> runstate.equals(RunState.RUNNING), this::fetchRunState); + }); + } catch (Exception e) { + Log.log(e); + } + } + + } + + @Override + public void setContext(AppContext context) { + this.refreshTracker.complete(context.getRefreshTracker()); + } + + @Override + public void delete() throws Exception { + DockerClient client = getTarget().getClient(); + if (client != null) { + RefreshStateTracker rt = this.refreshTracker.get(); + rt.run("Deleting " + getShortHash(), () -> { + debug("Deleting"); + client.removeContainerCmd(container.getId()).withForce(true).exec(); + debug("Waiting for Deleting"); + + RetryUtil.until(100, WAIT_BEFORE_KILLING.toMillis(), + exception -> exception instanceof NotFoundException, () -> { + try { + client.inspectContainerCmd(container.getId()).exec(); + } catch (Exception e) { + return e; + } + return null; + }); + debug("Deleted"); + }); + } + } + + @Override + public int getActualInstances() { + return fetchRunState().isActive() ? 1 : 0; + } + + @Override + public int getDebugPort() { + try { + if (fetchRunState().isActive()) { + String portStr = container.getLabels().get(DockerApp.DEBUG_PORT); + if (portStr!=null) { + int port = Integer.valueOf(portStr); + if (port>0) { + return port; + } + } + } + } catch (Exception e) { + Log.log(e); + } + return -1; + } + + @Override + public IProject getProject() { + try { + String projectName = container.getLabels().get(DockerApp.APP_NAME); + if (projectName!=null) { + return ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + @Override + public String getDevtoolsSecret() { + Map sysprops = getSystemProps(container); + return sysprops.getOrDefault(DevtoolsUtil.REMOTE_SECRET_PROP, null); + } + + private static final YamlTraversal dependencyNamePath = YamlPath.EMPTY + .thenValAt("bom") + .thenAnyChild() + .thenValAt("metadata") + .thenValAt("dependencies") + .thenAnyChild() + .thenValAt("name"); + + private Supplier hasDevtoolsDep; + + @Override + public boolean hasDevtoolsDependency() { + return this.hasDevtoolsDep.get(); + } + + // for debugging... keep in comments for now +// private void findNode(Object json, Predicate test, String path) { +// if (json instanceof String) { +// if (test.apply((String) json)) { +// System.out.println(path+" = "+json); +// } +// } else if (json instanceof Map) { +// Map map = (Map) json; +// for (Entry me : map.entrySet()) { +// findNode(me.getValue(), test, path+"."+escape(me.getKey())); +// } +// } else if (json instanceof List) { +// List list = (List) json; +// int index = 0; +// for (Object object : list) { +// findNode(object, test, path+"["+index+"]"); +// index++; +// } +// } +// +// } +// +// private String escape(String path) { +// if (path.contains(".")) { +// return "["+path+"]"; +// } +// return path; +// } + + public Map getSystemProps() { + return container!=null ? getSystemProps(container) : null; + } + + @SuppressWarnings("unchecked") + public static Map getSystemProps(Container c) { + try { + ObjectMapper mapper = new ObjectMapper(); + String sysprops = c.getLabels().get(DockerApp.SYSTEM_PROPS); + if (StringUtils.hasText(sysprops)) { + return mapper.readValue(sysprops, Map.class); + } + } catch (Exception e) { + Log.log(e); + } + return ImmutableMap.of(); + } + + @Override + public LogConnection connectLog(AppConsole logConsole, boolean includeHistory) { + return connectLog(target, container.getId(), logConsole, includeHistory); + } + + /** + * For debugging purposes. Keep track of number of active log handlers. So we + * can log this info and check whether log handlers are closed properly. + */ + private static AtomicInteger activeLogHandlers = new AtomicInteger(); + + private static class LogHandler implements ResultCallback{ + + private AtomicBoolean isClosed = new AtomicBoolean(); + + private OutputStream consoleOut; + private OutputStream consoleErr; + + LogConnection connection = new LogConnection() { + + @Override + public void dispose() { + LogHandler.this.close(); + } + + @Override + public boolean isClosed() { + return isClosed.get(); + } + }; + + private final CompletableFuture closeable = new CompletableFuture(); + + private DockerClient dedicatedClient; + + public LogHandler(DockerClient dedicatedClient, AppConsole console) { + this.dedicatedClient = dedicatedClient; + consoleOut = console.getOutputStream(LogType.APP_OUT); + consoleErr = console.getOutputStream(LogType.APP_OUT); + Log.info("Creating log handler. Now active: "+activeLogHandlers.incrementAndGet()); + } + + @Override + public void close() { + if (isClosed.compareAndSet(false, true)) { + closeable.thenAccept(c -> { + try { + c.close(); + } catch (IOException e) { + } + }); + try { + consoleOut.close(); + } catch (IOException e) { + } + try { + consoleErr.close(); + } catch (IOException e) { + } + try { + Log.info("Closing log handler. Now active: "+activeLogHandlers.decrementAndGet()); + dedicatedClient.close(); + } catch (IOException e) { + //ignore + } + } + } + + @Override + public void onStart(Closeable closeable) { + this.closeable.complete(closeable); + } + + @Override + public void onNext(Frame logMsg) { + try { + StreamType tp = logMsg.getStreamType(); + if (tp==StreamType.STDERR) { + consoleErr.write(logMsg.getPayload()); + } else if (tp==StreamType.STDOUT) { + consoleOut.write(logMsg.getPayload()); + } else { + Log.warn("Unknown docker log frame type dropped: "+tp); + } + } catch (Exception e) { + Log.log(e); + } + } + + @Override + public void onError(Throwable e) { + try { + consoleErr.write((ExceptionUtil.getMessage(e)+"\n").getBytes("UTF8")); + } catch (Exception e1) { + Log.log(e1); + } + Log.log(e); + this.close(); + } + + @Override + public void onComplete() { + this.close(); + } + } + + public static LogConnection connectLog(DockerRunTarget target, String containerId, AppConsole console, boolean includeHistory) { + DockerClient client = target.getDedicatedClientInstance(); + //Uses a dedicated client for log streaming because java docker client will eventually run out of connections in connection pool + //otherwise. + //See: + // - https://www.pivotaltracker.com/n/projects/1346850 + // - https://github.com/docker-java/docker-java/issues/1466 + if (client!=null) { + LogContainerCmd cmd = client.logContainerCmd(containerId) + .withStdOut(true).withStdErr(true).withFollowStream(true); + + if (!includeHistory) { + cmd = cmd.withSince((int)Instant.now().getEpochSecond()); + } + + LogHandler logHandler = cmd.exec(new LogHandler(client, console)); + return logHandler.connection; + } + return null; + } + + @Override + public ImageDescriptor getRunStateIcon(RunState runState) { + try { + if (RUNSTATE_ICONS==null) { + RUNSTATE_ICONS = ImmutableMap.of( + RunState.RUNNING, imageDescriptorFromPlugin(PLUGIN_ID, "/icons/container_started.png"), + RunState.INACTIVE, imageDescriptorFromPlugin(PLUGIN_ID, "/icons/container_stopped.png"), + RunState.DEBUGGING, imageDescriptorFromPlugin(PLUGIN_ID, "/icons/container_debugging.png"), + RunState.PAUSED, imageDescriptorFromPlugin(PLUGIN_ID, "/icons/container_paused.png") + ); + } + } catch (Exception e) { + Log.log(e); + } + if (RUNSTATE_ICONS!=null) { + return RUNSTATE_ICONS.get(runState); + } + return null; + } + + @Override + public String getConsoleDisplayName() { + return app.getName() + " - in container "+getStyledName(null).getString()+" @ "+getTarget().getName(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeployer.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeployer.java new file mode 100644 index 000000000..fc9bfed82 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeployer.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2020 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.docker.runtarget; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.model.Container; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +public class DockerDeployer extends AbstractDisposable { + + private final DockerDeployments deployments; + private final DockerClient client; + private Map apps = new HashMap<>(); + private final DockerRunTarget target; + + public DockerDeployer(DockerRunTarget target, DockerDeployments deployments, DockerClient client) { + this.target = target; + this.deployments = deployments; + this.client = client; + this.deployments.addListener(this, new DockerDeployments.Listener() { + @Override + public void added(DockerDeployment d) { + createOrUpdateApp(d); + } + + @Override + public void updated(DockerDeployment d) { + createOrUpdateApp(d); + } + + @Override + public void removed(DockerDeployment d) { + destroyDeployment(d); + } + }); + } + + synchronized private CompletableFuture createOrUpdateApp(DockerDeployment d) { + DockerApp app = apps.computeIfAbsent(d.getName(), name -> new DockerApp(name, target, client)); + return app.synchronizeWithDeployment(); + } + + synchronized private void destroyDeployment(DockerDeployment d) { + DockerApp app = apps.get(d.getName()); + if (app != null) { + if (client != null) { + try { + List images = app.fetchChildren(); + for (Container container : client.listContainersCmd() + .withShowAll(true) + .withLabelFilter(ImmutableMap.of(DockerApp.APP_NAME, d.getName())) + .exec() + ) { + client.removeContainerCmd(container.getId()).withForce(true).exec(); + } + for (App _img : images) { + DockerImage img = (DockerImage) _img; + try { + client.removeImageCmd(img.getName()).withForce(true).withNoPrune(false).exec(); + } catch (Exception e) { + Log.log(e); + } + } + apps.remove(d.getName()); + } catch (Exception e) { + Log.log(e); + } + } + } + } + + synchronized public Collection getApps() { + return ImmutableList.copyOf(apps.values()); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeployment.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeployment.java new file mode 100644 index 000000000..af1e8176e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeployment.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * Copyright (c) 2020 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.docker.runtarget; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.ide.eclipse.boot.dash.api.DesiredInstanceCount; +import org.springframework.ide.eclipse.boot.dash.model.Nameable; +import org.springframework.ide.eclipse.boot.dash.model.RunState; + +import com.google.common.collect.ImmutableMap; + +/** + * Data class containing info about a 'deployment' (i.e. what gets created when you + * drag and drop a project onto a docker target. + *

+ * This is similar in concept to a k8s deployment. It is an object that describes a + * indented state of some containers running in docker. It contains all the + * information needed to create the container. + *

+ * This object must be easy to serialize as it needs to be persisted across Eclipse + * restarts. + */ +public class DockerDeployment implements Nameable, DesiredInstanceCount { + + private String name; // name of the deployment, currently this is also the name of the deployed project. + private RunState runState; + private String buildId; + + /** + * Records the DockerRunTarget 'sessionId' that was in effect when this deployment was created. + * This allows for deployment synchronization to distinguish between a deployment created in the + * current session or an older deployment (restored from persistent data). + *

+ * See: https://www.pivotaltracker.com/story/show/173136687 + */ + private String sessionId; + + private HashMap sysprops; + + public DockerDeployment() {} + + public DockerDeployment(DockerDeployment copyFrom) { + this.name = copyFrom.name; + this.runState = copyFrom.runState; + this.buildId = copyFrom.buildId; + this.sessionId = copyFrom.sessionId; + this.sysprops = copyFrom.sysprops; + if (sysprops!=null) { + sysprops = new HashMap<>(sysprops); + } + } + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public RunState getRunState() { + return runState; + } + public void setRunState(RunState runState) { + this.runState = runState; + } + + public DockerDeployment withGoalState(RunState newGoalState, String sessionId) { + DockerDeployment d = new DockerDeployment(this); + d.setRunState(newGoalState); + d.setSessionId(sessionId); + return d; + } + + public String getBuildId() { + return buildId; + } + + public Map getSystemProperties() { + if (sysprops == null) { + return ImmutableMap.of(); + } + return sysprops; + } + + /** + * When you set a buildId you should *also* set the session id as well. These two id's are pair that + * allways belong together. The only scenario were this method is called 'by itself' is by Yaml deserialization. + */ + public void setBuildId(String buildId) { + this.buildId = buildId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public String getSessionId() { + return sessionId; + } + + public int getDesiredInstances() { + return 1; + } + + public void setSystemProperty(String name, String value) { + if (sysprops==null) { + sysprops = new HashMap<>(); + } + if (value==null) { + sysprops.remove(name); + } else { + sysprops.put(name, value); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeploymentList.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeploymentList.java new file mode 100644 index 000000000..c296fb57c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeploymentList.java @@ -0,0 +1,27 @@ +package org.springframework.ide.eclipse.boot.dash.docker.runtarget; + +import java.util.Collection; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +public class DockerDeploymentList { + + private List deployments; + + public DockerDeploymentList() { + } + + public DockerDeploymentList(Collection values) { + this.deployments = ImmutableList.copyOf(values); + } + + public List getDeployments() { + return deployments; + } + + public void setDeployments(List deployments) { + this.deployments = deployments; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeployments.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeployments.java new file mode 100644 index 000000000..4723c8763 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerDeployments.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * Copyright (c) 2020 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.docker.runtarget; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.frameworks.core.util.StringUtils; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +import org.springsource.ide.eclipse.commons.livexp.core.LiveCounter; +import org.springsource.ide.eclipse.commons.livexp.core.OnDispose; +import org.springsource.ide.eclipse.commons.livexp.util.Log; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.CustomClassLoaderConstructor; + +public class DockerDeployments extends AbstractDisposable { + + public interface Listener { + void added(DockerDeployment d); + void removed(DockerDeployment d); + void updated(DockerDeployment d); + } + + private static final String PERSISTENCE_KEY = DockerDeployments.class.getName(); + private static final Listener[] NO_LISTENERS = {}; + private static final DockerDeployment[] NO_DEPLOYMENTS = {}; + private final Map byName = new HashMap<>(); + private final PropertyStoreApi persistentProperties; + private LiveCounter persistTrigger = new LiveCounter(); + private List listeners = new ArrayList<>(); + + public DockerDeployments(PropertyStoreApi persistentProperties) { + this.persistentProperties = persistentProperties; + restore(); + persistTrigger.onChange(this, (_e, _v) -> persist()); + } + + private void restore() { + Yaml yaml = yaml(); + String serialized = persistentProperties.get(PERSISTENCE_KEY); + try { + if (StringUtils.hasText(serialized)) { + DockerDeploymentList deserialized = yaml.loadAs(serialized, DockerDeploymentList.class); + for (DockerDeployment d : deserialized.getDeployments()) { + byName.put(d.getName(), d); + } + persistTrigger.increment(); + } + } catch (Exception e) { + Log.log(e); + } + } + + private synchronized void persist() { + Yaml yaml = yaml(); + String serialized = yaml.dump(new DockerDeploymentList(byName.values())); + try { + persistentProperties.put(PERSISTENCE_KEY, serialized); + } catch (Exception e) { + Log.log(e); + } + } + + private Yaml yaml() { + return new Yaml(new CustomClassLoaderConstructor(DockerDeploymentList.class, DockerDeployments.class.getClassLoader())); + } + + public void createOrUpdate(DockerDeployment deployment) { + Listener[] listeners = NO_LISTENERS; + DockerDeployment old = null; + synchronized (this) { + old = byName.put(deployment.getName(), deployment); + listeners = this.listeners.toArray(NO_LISTENERS); + } + for (Listener l : listeners ) { + if (old==null) { + l.added(deployment); + } else { + l.updated(deployment); + } + } + persistTrigger.increment(); + } + + public void remove(String name) { + Listener[] listeners = NO_LISTENERS; + DockerDeployment removed = null; + synchronized (this) { + removed = byName.remove(name); + if (removed!=null) { + listeners = this.listeners.toArray(NO_LISTENERS); + } + } + for (Listener l : listeners ) { + l.removed(removed); + } + persistTrigger.increment(); + } + + public void addListener(OnDispose owner, Listener l) { + DockerDeployment[] deployments = NO_DEPLOYMENTS; + synchronized (this) { + listeners.add(l); + deployments = byName.values().toArray(NO_DEPLOYMENTS); + } + owner.onDispose(d -> removeListener(l)); + for (DockerDeployment d : deployments) { + l.added(d); + } + } + + private synchronized void removeListener(Listener l) { + listeners.remove(l); + } + + public synchronized DockerDeployment get(String name) { + return byName.get(name); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerImage.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerImage.java new file mode 100644 index 000000000..15dd68d98 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerImage.java @@ -0,0 +1,259 @@ +/******************************************************************************* + * Copyright (c) 2020 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.docker.runtarget; + +import static org.eclipse.ui.plugin.AbstractUIPlugin.imageDescriptorFromPlugin; +import static org.springframework.ide.eclipse.boot.dash.docker.runtarget.DockerRunTargetType.PLUGIN_ID; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.linuxtools.docker.core.IDockerImage; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.AppContext; +import org.springframework.ide.eclipse.boot.dash.api.Deletable; +import org.springframework.ide.eclipse.boot.dash.api.DevtoolsConnectable; +import org.springframework.ide.eclipse.boot.dash.api.ProjectRelatable; +import org.springframework.ide.eclipse.boot.dash.api.RunStateIconProvider; +import org.springframework.ide.eclipse.boot.dash.api.Styleable; +import org.springframework.ide.eclipse.boot.dash.api.TemporalBoolean; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.remote.ChildBearing; +import org.springframework.ide.eclipse.boot.dash.model.remote.RefreshStateTracker; +import org.springframework.ide.eclipse.boot.util.RetryUtil; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.frameworks.core.util.JobUtil; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.exception.NotFoundException; +import com.github.dockerjava.api.model.Container; +import com.github.dockerjava.api.model.Image; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import com.google.common.collect.ImmutableMap; + +public class DockerImage implements App, ChildBearing, Styleable, ProjectRelatable, + RunStateIconProvider, Deletable, DevtoolsConnectable +{ + + private final DockerApp app; + private final Image image; + public final CompletableFuture refreshTracker = new CompletableFuture<>(); + private final Supplier hasDevtoolsDependency; + + private static Map RUNSTATE_ICONS = null; + + public DockerImage(DockerApp app, Image image) { + this.app = app; + this.image = image; + this.hasDevtoolsDependency = DockerContainer.hasDevtoolsDependency(image::getLabels); + } + + @Override + public void setContext(AppContext context) { + this.refreshTracker.complete(context.getRefreshTracker()); + } + + @Override + public String getName() { + return image.getId(); + } + + @Override + public DockerRunTarget getTarget() { + return this.app.getTarget(); + } + + @Override + public List fetchChildren() throws Exception { + Builder builder = ImmutableList.builder(); + DockerClient client = app.getClient(); + if (client!=null) { + List containers = JobUtil.interruptAfter(Duration.ofSeconds(15), + () -> client.listContainersCmd() + .withShowAll(true) + .withAncestorFilter(ImmutableList.of(image.getId())) + .withLabelFilter(ImmutableMap.of(DockerApp.APP_NAME, app.getName())) + .exec() + ); + for (Container container : containers) { + builder.add(new DockerContainer(getTarget(), app, container)); + } + } + return builder.build(); + } + + /** + * @param dockerImage + * the {@link IDockerImage} to process + * @return the {@link StyledString} to be displayed. + */ +// public static StyledString getStyledText(final IDockerImage dockerImage) { +// final StyledString result = new StyledString(dockerImage.repo()); +// if (!dockerImage.tags().isEmpty()) { +// final List tags = new ArrayList<>(dockerImage.tags()); +// Collections.sort(tags); +// result.append(":"); +// result.append(tags.stream().collect(Collectors.joining(", ")), //$NON-NLS-1$ +// StyledString.COUNTER_STYLER); +// } +// // TODO: remove the cast to 'DockerImage' once the 'shortId()' +// // method is in the public API +// result.append(" (", StyledString.QUALIFIER_STYLER) //$NON-NLS-1$ +// .append(((DockerImage) dockerImage).shortId(), +// StyledString.QUALIFIER_STYLER) +// .append(')', StyledString.QUALIFIER_STYLER); // $NON-NLS-1$ +// return result; +// } + + @Override + public StyledString getStyledName(Stylers stylers) { + List repoTags = Arrays.asList(image.getRepoTags()); + String repo = extractRepo(repoTags); + List tags = extractTags(repoTags); + final StyledString result = new StyledString(repo); + if (!tags.isEmpty()) { + result.append(":"); + result.append(tags.stream().collect(Collectors.joining(", ")), + StyledString.COUNTER_STYLER); + } + result + .append(" (", StyledString.QUALIFIER_STYLER) + .append(getShortHash(), StyledString.QUALIFIER_STYLER) + .append(')', StyledString.QUALIFIER_STYLER); + return result; + } + + private List extractTags(List repoTags) { + if (repoTags!=null && !repoTags.isEmpty()) { + ArrayList tags = new ArrayList<>(); + for (String repoTag : repoTags) { + int colon = repoTag.indexOf(':'); + if (colon>=0) { + String tag = repoTag.substring(colon+1); + tags.add(tag); + } + } + Collections.sort(tags); + return tags; + } + return ImmutableList.of(); + } + + private String extractRepo(List repoTags) { + if (repoTags!=null && !repoTags.isEmpty()) { + String repoTag = repoTags.get(0); + int colon = repoTag.indexOf(':'); + if (colon>=0) { + return repoTag.substring(0, colon); + } + } + return null; + } + + private String getShortHash() { + String id = StringUtil.removePrefix(image.getId(), "sha256:"); + if (id.length() > 12) { + id = id.substring(0, 12); + } + return id; + } + + @Override + public String toString() { + return "DockerImage("+image.getId()+")"; + } + + @Override + public IProject getProject() { + return app.getProject(); + } + + @Override + public ImageDescriptor getRunStateIcon(RunState runState) { + try { + if (RUNSTATE_ICONS==null) { + RUNSTATE_ICONS = ImmutableMap.of( + RunState.RUNNING, imageDescriptorFromPlugin(PLUGIN_ID, "/icons/image_started.png"), + RunState.INACTIVE, imageDescriptorFromPlugin(PLUGIN_ID, "/icons/image_stopped.png"), + RunState.DEBUGGING, imageDescriptorFromPlugin(PLUGIN_ID, "/icons/image_debugging.png"), + RunState.PAUSED, imageDescriptorFromPlugin(PLUGIN_ID, "/icons/image_paused.png") + ); + } + } catch (Exception e) { + Log.log(e); + } + if (RUNSTATE_ICONS!=null) { + return RUNSTATE_ICONS.get(runState); + } + return null; + } + @Override + public void delete() throws Exception { + DockerClient client = getTarget().getClient(); + if (client != null) { + RefreshStateTracker rt = this.refreshTracker.get(); + rt.run("Deleting " + getShortHash(), () -> { + //Delete containers (if there are running containers, 'force' option on removeImage + // will not work. + for (Container container : client.listContainersCmd() + .withShowAll(true) + .withFilter("ancestor", ImmutableList.of(image.getId())) + .exec() + ) { + client.removeContainerCmd(container.getId()).withForce(true).exec(); + } + + + client.removeImageCmd(getName()).withForce(true).withNoPrune(false).exec(); + + RetryUtil.until(100, DockerContainer.WAIT_BEFORE_KILLING.toMillis(), + exception -> exception instanceof NotFoundException, + () -> { + try { + client.inspectImageCmd(image.getId()).exec(); + } catch (Exception e) { + return e; + } + return null; + } + ); + }); + } + } + + @Override + public String getDevtoolsSecret() { + return null; + } + + @Override + public boolean hasDevtoolsDependency() { + return this.hasDevtoolsDependency.get(); + } + + @Override + public TemporalBoolean isDevtoolsConnectable() { + return TemporalBoolean.NEVER; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerRunTarget.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerRunTarget.java new file mode 100644 index 000000000..451b6a386 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerRunTarget.java @@ -0,0 +1,208 @@ +/******************************************************************************* + * Copyright (c) 2020 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.docker.runtarget; + +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.DEFAULT_PATH; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.DEVTOOLS; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.INSTANCES; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.LIVE_PORT; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.NAME; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.PROGRESS; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.RUN_STATE_ICN; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.TAGS; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import org.eclipse.core.resources.IProject; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.DebuggableTarget; +import org.springframework.ide.eclipse.boot.dash.api.ProjectDeploymentTarget; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.RemoteBootDashModel; +import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsUtil; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.docker.ui.DefaultDockerUserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.AbstractRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTarget; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.util.OldValueDisposer; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateNetworkResponse; +import com.github.dockerjava.api.model.Network; +import com.google.common.collect.ImmutableList; + +public class DockerRunTarget extends AbstractRunTarget +implements RemoteRunTarget, ProjectDeploymentTarget, DebuggableTarget { + + private static final String DOCKER_NETWORK_NAME = "sts-bootdash"; + + final LiveVariable client; + LiveExpression sessionId; + + private DockerTargetParams params; + + final DockerDeployments deployments; + private final LiveExpression deployer; + + @SuppressWarnings("resource") + public DockerRunTarget(DockerRunTargetType type, DockerTargetParams params, DockerClient client) { + super(type, params.getUri()); + this.client = new OldValueDisposer(this).getVar(); + this.sessionId = this.client.apply(c -> c!=null ? UUID.randomUUID().toString() : null); + this.deployments = this.client.addDisposableChild(new DockerDeployments(getPersistentProperties())); + this.params = params; + this.client.setValue(client); + this.deployer = this.client.applyFactory(c -> new DockerDeployer(DockerRunTarget.this, deployments, c)); + } + + public SimpleDIContext injections() { + return getType().injections(); + } + + @Override + public DockerRunTargetType getType() { + return (DockerRunTargetType) super.getType(); + } + + @Override + public RemoteBootDashModel createSectionModel(BootDashViewModel parent) { + return new GenericRemoteBootDashModel<>(this, parent); + } + + @Override + public boolean canRemove() { + return true; + } + + @Override + public boolean canDeployAppsFrom() { + return false; + } + + @Override + public DockerTargetParams getParams() { + return params; + } + + @Override + public LiveExpression getClientExp() { + return client; + } + + @Override + public Collection fetchApps() throws Exception { + DockerDeployer deployer = this.deployer.getValue(); + return deployer == null ? ImmutableList.of() : deployer.getApps(); + } + + @Override + public synchronized void disconnect() { + DockerClient c = client.getValue(); + if (c!=null) { + client.setValue(null); + } + } + + @Override + public synchronized void connect(ConnectMode mode) throws Exception { + if (!isConnected()) { + try { + DockerClient c = DockerRunTargetType.createDockerClient(params.getUri()); + c.infoCmd().exec(); //ensure docker daemon is reachable. + this.client.setValue(c); + } catch (Error e) { + DefaultDockerUserInteractions.openBundleWiringError(e); + } + } + } + + /** + * Obtain a client instance for 'exclusive' use. This creates a new client instance which only belongs + * to whoever called this method. It is the responsibility of the caller + * to close the instance when it is no longer in use. + */ + public DockerClient getDedicatedClientInstance() { + if (isConnected()) { + return DockerRunTargetType.createDockerClient(params.getUri()); + } + return null; + } + + + @Override + public void performDeployment(Set projects, RunState runOrDebug) throws Exception { + for (IProject p : projects) { + DockerDeployment d = new DockerDeployment(); + d.setName(p.getName()); + d.setRunState(runOrDebug); + d.setSessionId(sessionId.getValue()); + d.setBuildId(UUID.randomUUID().toString()); + d.setSystemProperty(DevtoolsUtil.REMOTE_SECRET_PROP, DevtoolsUtil.getSecret(p)); + deployments.createOrUpdate(d); + } + } + + @Override + public BootDashColumn[] getDefaultColumns() { + return new BootDashColumn[] { + RUN_STATE_ICN, + NAME, + PROGRESS, + LIVE_PORT, + DEVTOOLS, + INSTANCES, + DEFAULT_PATH, + TAGS + }; + } + + public String getSessionId() { + return sessionId.getValue(); + } + + @Override + public boolean isDebuggingSupported() { + return true; + } + + public synchronized Network ensureNetwork() { + DockerClient client = this.client.getValue(); + if (client!=null) { + Network network = getNetwork(client, DOCKER_NETWORK_NAME); + if (network!=null) { + return network; + } + client.createNetworkCmd().withName(DOCKER_NETWORK_NAME).withDriver("bridge").exec(); + return getNetwork(client, DOCKER_NETWORK_NAME); + } + return null; + } + + private Network getNetwork(DockerClient client, String name) { + List networks = client.listNetworksCmd().withNameFilter(DOCKER_NETWORK_NAME).exec(); + for (Network network : networks) { + if (network.getName().equals(name)) { + return network; + } + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerRunTargetType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerRunTargetType.java new file mode 100644 index 000000000..0744bce0e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerRunTargetType.java @@ -0,0 +1,166 @@ +/******************************************************************************* + * Copyright (c) 2020 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.docker.runtarget; + +import static org.eclipse.ui.plugin.AbstractUIPlugin.imageDescriptorFromPlugin; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.docker.ui.DefaultDockerUserInteractions; +import org.springframework.ide.eclipse.boot.dash.docker.ui.DockerUserInteractions; +import org.springframework.ide.eclipse.boot.dash.docker.ui.SelectDockerDaemonDialog; +import org.springframework.ide.eclipse.boot.dash.docker.ui.SelectDockerDaemonDialog.Model; +import org.springframework.ide.eclipse.boot.dash.model.MissingLiveInfoMessages; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.AbstractRemoteRunTargetType; +import org.springframework.ide.eclipse.editor.support.util.HtmlSnippet; +import org.springsource.ide.eclipse.commons.frameworks.core.util.JobUtil; +import org.springsource.ide.eclipse.commons.frameworks.core.util.StringUtils; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientImpl; +import com.github.dockerjava.transport.DockerHttpClient; +import com.github.dockerjava.zerodep.ZerodepDockerHttpClient; + +public class DockerRunTargetType extends AbstractRemoteRunTargetType { + + public static final String PLUGIN_ID = "org.springframework.ide.eclipse.boot.dash.docker"; + + public DockerRunTargetType(SimpleDIContext injections) { + super(injections, "Docker"); + } + + @Override + public CompletableFuture openTargetCreationUi(LiveSetVariable targets) { + return JobUtil.runInJob("Docker Target Creation", mon -> { + DockerRunTarget target = login(targets); + if (target!=null) { + targets.add(target); + } + }); + } + + private DockerRunTarget login(LiveSetVariable targets) { + String uri = inputDockerUrl(); + if (StringUtils.hasText(uri)) { + Set existing = new HashSet<>(targets.getValues().size()); + for (RunTarget t : targets.getValues()) { + if (t instanceof DockerRunTarget) { + DockerRunTarget dt = (DockerRunTarget) t; + existing.add(dt.getParams().getUri()); + } + } + if (existing.contains(uri)) { + ui().errorPopup("Duplicate Target", "A target with the same uri ("+uri+") already exists!"); + } else { + try { + DockerClient client = createDockerClient(uri); + return new DockerRunTarget(this, new DockerTargetParams(uri), client); + } catch (Error e) { + DefaultDockerUserInteractions.openBundleWiringError(e); + } + } + } + return null; + } + + public static DockerClient createDockerClient(String uri) { + DefaultDockerClientConfig conf = DefaultDockerClientConfig.createDefaultConfigBuilder().withDockerHost(uri).build(); + + DockerHttpClient httpClient = new ZerodepDockerHttpClient.Builder() + .dockerHost(conf.getDockerHost()) + .sslConfig(conf.getSSLConfig()) + .build(); + return DockerClientImpl.getInstance(conf, httpClient); + } + + private String inputDockerUrl() { + DockerUserInteractions ui = injections().getBean(DockerUserInteractions.class); + Model model = new SelectDockerDaemonDialog.Model(); + ui.selectDockerDaemonDialog(model); + if (model.okPressed.getValue()) { + return model.daemonUrl.getValue(); + } else { + return null; + } + } + + @Override + public RunTarget createRunTarget(DockerTargetParams params) { + return new DockerRunTarget(this, params, null); + } + + @Override + public ImageDescriptor getIcon() { + return imageDescriptorFromPlugin(PLUGIN_ID, "/icons/docker.png"); + } + + @Override + public ImageDescriptor getDisconnectedIcon() { + return imageDescriptorFromPlugin(PLUGIN_ID, "/icons/docker-inactive.png"); + } + + + @Override + public DockerTargetParams parseParams(String uri) { + return new DockerTargetParams(uri); + } + + @Override + public String serialize(DockerTargetParams p) { + return p==null ? null : p.getUri(); + } + + @Override + public MissingLiveInfoMessages getMissingLiveInfoMessages() { + return new MissingLiveInfoMessages() { + @Override + public HtmlSnippet getMissingInfoMessage(String appName, String actuatorEndpoint) { + + return buffer -> { + buffer.raw("

"); + buffer.raw(""); + buffer.text(appName); + buffer.raw(""); + buffer.text(" must be running with JMX and actuator endpoint enabled:"); + buffer.raw("

"); + + buffer.raw("
    "); + + buffer.raw("
  1. "); + buffer.text("Enable actuator."); + buffer.raw("
  2. "); + + buffer.raw("
  3. "); + buffer.text("Enable actuator endpoint "); + buffer.raw(""); + buffer.text(actuatorEndpoint); + buffer.raw(""); + buffer.text(" in the application."); + buffer.raw("
  4. "); + buffer.raw("
"); + + buffer.href(EXTERNAL_DOCUMENT_LINK, "See documentation"); + }; + + } + }; + } + + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerTargetParams.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerTargetParams.java new file mode 100644 index 000000000..82108bfad --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/runtarget/DockerTargetParams.java @@ -0,0 +1,48 @@ +package org.springframework.ide.eclipse.boot.dash.docker.runtarget; + +public class DockerTargetParams { + + private final String uri; + + public DockerTargetParams(String uri) { + super(); + this.uri = uri; + } + + public String getUri() { + return uri; + } + + @Override + public String toString() { + return uri; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((uri == null) ? 0 : uri.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + DockerTargetParams other = (DockerTargetParams) obj; + if (uri == null) { + if (other.uri != null) + return false; + } else if (!uri.equals(other.uri)) + return false; + return true; + } + + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/DefaultDockerUserInteractions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/DefaultDockerUserInteractions.java new file mode 100644 index 000000000..0b42eb26a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/DefaultDockerUserInteractions.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) 2020 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.docker.ui; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.MultiStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.docker.ui.SelectDockerDaemonDialog.Model; +import org.springsource.ide.eclipse.commons.livexp.ui.UIContext; + +public class DefaultDockerUserInteractions implements DockerUserInteractions { + + private static final String MESSAGE_WIRING_ERROR = "This is embarassing... There was an error encountered in the OSGi setup as some bundles are incorrectly wired.\n" + + "\nPlease restart your IDE with the '-clean' parameter once to workaround the problem. It is shame of course that we could not automate the proper restart, apologies for the inconvenience.\n" + + "\nThe issue will be fixed in the next version of Eclipse (4.17)"; + private SimpleDIContext injections; + + public DefaultDockerUserInteractions(SimpleDIContext injections) { + this.injections = injections;} + + @Override + public void selectDockerDaemonDialog(Model model) { + Display.getDefault().syncExec(() -> new SelectDockerDaemonDialog(model, getShell()).open()); + } + + private Shell getShell() { + return injections.getBean(UIContext.class).getShell(); + } + + public static void openBundleWiringError(Throwable t) { + List childStatuses = new ArrayList<>(t.getStackTrace().length); + for (StackTraceElement stackTrace : t.getStackTrace()) { + Status status = new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, stackTrace.toString()); + childStatuses.add(status); + } + + MultiStatus status = new MultiStatus(BootDashActivator.PLUGIN_ID, IStatus.ERROR, + childStatuses.toArray(new Status[] {}), t.toString(), t); + Display display = Display.getCurrent(); + if (display != null && !display.isDisposed()) { + ErrorDialog.openError(display.getActiveShell(), "Error Wiring Bundles", MESSAGE_WIRING_ERROR, status); + } else { + display = PlatformUI.getWorkbench().getDisplay(); + if (display != null && !display.isDisposed()) { + display.asyncExec(() -> ErrorDialog.openError(Display.getCurrent().getActiveShell(), "Error Wiring Bundles", MESSAGE_WIRING_ERROR, status)); + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/DockerUserInteractions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/DockerUserInteractions.java new file mode 100644 index 000000000..9cc17936d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/DockerUserInteractions.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2020 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.docker.ui; + +public interface DockerUserInteractions { + void selectDockerDaemonDialog(SelectDockerDaemonDialog.Model model); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/ExtraDockerActions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/ExtraDockerActions.java new file mode 100644 index 000000000..182578f2c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/ExtraDockerActions.java @@ -0,0 +1,38 @@ +package org.springframework.ide.eclipse.boot.dash.docker.ui; + +import java.util.Collection; + +import org.eclipse.core.runtime.Platform; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.docker.actions.OpenDockerExplorerAction; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveProcessCommandsExecutor; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashAction; +import org.springframework.ide.eclipse.boot.dash.views.BootDashActions; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +public class ExtraDockerActions implements BootDashActions.Factory { + + @Override + public Collection create( + BootDashActions actions, + BootDashViewModel model, + MultiSelection selection, + LiveExpression section, + SimpleDIContext context, + LiveProcessCommandsExecutor liveProcessCmds) { + Builder contributions = ImmutableList.builder(); + if (section!=null) { + if (Platform.getBundle("org.eclipse.linuxtools.docker.ui")!=null) { + contributions.add(new OpenDockerExplorerAction(section, context)); + } + } + return contributions.build(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/SelectDockerDaemonDialog.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/SelectDockerDaemonDialog.java new file mode 100644 index 000000000..2f0662eb8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.docker/src/org/springframework/ide/eclipse/boot/dash/docker/ui/SelectDockerDaemonDialog.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2020 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.docker.ui; + +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.swt.widgets.Shell; +import org.springsource.ide.eclipse.commons.core.util.OsUtils; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.StringFieldModel; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.ui.CheckboxSection; +import org.springsource.ide.eclipse.commons.livexp.ui.DialogWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.OkButtonHandler; +import org.springsource.ide.eclipse.commons.livexp.ui.StringFieldSection; +import org.springsource.ide.eclipse.commons.livexp.ui.WizardPageSection; + +import com.google.common.collect.ImmutableList; + +public class SelectDockerDaemonDialog extends DialogWithSections { + + private Model model; + + public static class Model implements OkButtonHandler { + private static final String DEFAULT_UNIX_DOCKER_URL = "unix:///var/run/docker.sock"; + private static final String DEFAULT_WINDOWS_DOCKER_URL = "tcp://localhost:2375"; + + public final LiveVariable useLocalDaemon = new LiveVariable<>(true); + public final LiveExpression daemonUrlEnabled = useLocalDaemon.apply(local -> !local); + public final LiveExpression urlValidator = new LiveExpression(ValidationResult.OK) { + + @Override + protected ValidationResult compute() { + if (daemonUrl != null && isWindowsDefaultUrl(daemonUrl.getValue())) { + return ValidationResult.warning("May require exposing local Docker daemon in the Docker settings"); + } + return ValidationResult.OK; + } + }; + + public final StringFieldModel daemonUrl = new StringFieldModel("Url", getDefaultDaemonUrl()); + public final LiveVariable okPressed = new LiveVariable<>(false); + + { + daemonUrl.validator(urlValidator); + urlValidator.dependsOn(daemonUrl.getVariable()); + + daemonUrlEnabled.onChange((e,v) -> { + if (useLocalDaemon.getValue()) { + daemonUrl.setValue(getDefaultDaemonUrl()); + } + }); + } + + @Override + public void performOk() throws Exception { + okPressed.setValue(true); + } + + private String getDefaultDaemonUrl() { + if (OsUtils.isWindows()) { + return DEFAULT_WINDOWS_DOCKER_URL; + } else { + return DEFAULT_UNIX_DOCKER_URL; + } + } + + private boolean isWindowsDefaultUrl(String url) { + return OsUtils.isWindows() && DEFAULT_WINDOWS_DOCKER_URL.equals(url); + } + } + + public SelectDockerDaemonDialog(Model model, Shell shell) { + super("Connect to Docker Daemon", model, shell); + this.model = model; + } + + @SuppressWarnings("resource") @Override + protected List createSections() throws CoreException { + return ImmutableList.of( + new CheckboxSection(this, model.useLocalDaemon, "Use Local Daemon"), + new StringFieldSection(this, model.daemonUrl).setEnabler(model.daemonUrlEnabled) + ); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/.project new file mode 100644 index 000000000..0ecb273c0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/.project @@ -0,0 +1,17 @@ + + + org.springframework.ide.eclipse.boot.dash.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + + org.eclipse.pde.FeatureNature + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/build.properties b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/build.properties new file mode 100644 index 000000000..d5cc70b2c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/build.properties @@ -0,0 +1,9 @@ +bin.includes = feature.properties,\ + feature.xml,\ + epl-v10.html +src.includes = .project,\ + build.properties,\ + feature.properties,\ + feature.xml,\ + epl-v10.html +generate.feature@org.springframework.ide.eclipse.feature.source=org.springframework.ide.eclipse.feature diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/epl-v10.html b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/epl-v10.html new file mode 100644 index 000000000..ed4b19665 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/epl-v10.html @@ -0,0 +1,328 @@ + + + + + + + + +Eclipse Public License - Version 1.0 + + + + + + +
+ +

Eclipse Public License - v 1.0 +

+ +

THE ACCOMPANYING PROGRAM IS PROVIDED UNDER +THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, +REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE +OF THIS AGREEMENT.

+ +

1. DEFINITIONS

+ +

"Contribution" means:

+ +

a) +in the case of the initial Contributor, the initial code and documentation +distributed under this Agreement, and
+b) in the case of each subsequent Contributor:

+ +

i) +changes to the Program, and

+ +

ii) +additions to the Program;

+ +

where +such changes and/or additions to the Program originate from and are distributed +by that particular Contributor. A Contribution 'originates' from a Contributor +if it was added to the Program by such Contributor itself or anyone acting on +such Contributor's behalf. Contributions do not include additions to the +Program which: (i) are separate modules of software distributed in conjunction +with the Program under their own license agreement, and (ii) are not derivative +works of the Program.

+ +

"Contributor" means any person or +entity that distributes the Program.

+ +

"Licensed Patents " mean patent +claims licensable by a Contributor which are necessarily infringed by the use +or sale of its Contribution alone or when combined with the Program.

+ +

"Program" means the Contributions +distributed in accordance with this Agreement.

+ +

"Recipient" means anyone who +receives the Program under this Agreement, including all Contributors.

+ +

2. GRANT OF RIGHTS

+ +

a) +Subject to the terms of this Agreement, each Contributor hereby grants Recipient +a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly +display, publicly perform, distribute and sublicense the Contribution of such +Contributor, if any, and such derivative works, in source code and object code +form.

+ +

b) +Subject to the terms of this Agreement, each Contributor hereby grants +Recipient a non-exclusive, worldwide, royalty-free +patent license under Licensed Patents to make, use, sell, offer to sell, import +and otherwise transfer the Contribution of such Contributor, if any, in source +code and object code form. This patent license shall apply to the combination +of the Contribution and the Program if, at the time the Contribution is added +by the Contributor, such addition of the Contribution causes such combination +to be covered by the Licensed Patents. The patent license shall not apply to +any other combinations which include the Contribution. No hardware per se is +licensed hereunder.

+ +

c) +Recipient understands that although each Contributor grants the licenses to its +Contributions set forth herein, no assurances are provided by any Contributor +that the Program does not infringe the patent or other intellectual property +rights of any other entity. Each Contributor disclaims any liability to Recipient +for claims brought by any other entity based on infringement of intellectual +property rights or otherwise. As a condition to exercising the rights and +licenses granted hereunder, each Recipient hereby assumes sole responsibility +to secure any other intellectual property rights needed, if any. For example, +if a third party patent license is required to allow Recipient to distribute +the Program, it is Recipient's responsibility to acquire that license before +distributing the Program.

+ +

d) +Each Contributor represents that to its knowledge it has sufficient copyright +rights in its Contribution, if any, to grant the copyright license set forth in +this Agreement.

+ +

3. REQUIREMENTS

+ +

A Contributor may choose to distribute the +Program in object code form under its own license agreement, provided that: +

+ +

a) +it complies with the terms and conditions of this Agreement; and

+ +

b) +its license agreement:

+ +

i) +effectively disclaims on behalf of all Contributors all warranties and +conditions, express and implied, including warranties or conditions of title +and non-infringement, and implied warranties or conditions of merchantability +and fitness for a particular purpose;

+ +

ii) +effectively excludes on behalf of all Contributors all liability for damages, +including direct, indirect, special, incidental and consequential damages, such +as lost profits;

+ +

iii) +states that any provisions which differ from this Agreement are offered by that +Contributor alone and not by any other party; and

+ +

iv) +states that source code for the Program is available from such Contributor, and +informs licensees how to obtain it in a reasonable manner on or through a +medium customarily used for software exchange.

+ +

When the Program is made available in source +code form:

+ +

a) +it must be made available under this Agreement; and

+ +

b) a +copy of this Agreement must be included with each copy of the Program.

+ +

Contributors may not remove or alter any +copyright notices contained within the Program.

+ +

Each Contributor must identify itself as the +originator of its Contribution, if any, in a manner that reasonably allows +subsequent Recipients to identify the originator of the Contribution.

+ +

4. COMMERCIAL DISTRIBUTION

+ +

Commercial distributors of software may +accept certain responsibilities with respect to end users, business partners +and the like. While this license is intended to facilitate the commercial use +of the Program, the Contributor who includes the Program in a commercial +product offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes the +Program in a commercial product offering, such Contributor ("Commercial +Contributor") hereby agrees to defend and indemnify every other +Contributor ("Indemnified Contributor") against any losses, damages and +costs (collectively "Losses") arising from claims, lawsuits and other +legal actions brought by a third party against the Indemnified Contributor to +the extent caused by the acts or omissions of such Commercial Contributor in +connection with its distribution of the Program in a commercial product +offering. The obligations in this section do not apply to any claims or Losses +relating to any actual or alleged intellectual property infringement. In order +to qualify, an Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial Contributor +to control, and cooperate with the Commercial Contributor in, the defense and +any related settlement negotiations. The Indemnified Contributor may participate +in any such claim at its own expense.

+ +

For example, a Contributor might include the +Program in a commercial product offering, Product X. That Contributor is then a +Commercial Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance claims and +warranties are such Commercial Contributor's responsibility alone. Under this +section, the Commercial Contributor would have to defend claims against the +other Contributors related to those performance claims and warranties, and if a +court requires any other Contributor to pay any damages as a result, the +Commercial Contributor must pay those damages.

+ +

5. NO WARRANTY

+ +

EXCEPT AS EXPRESSLY SET FORTH IN THIS +AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, +WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, +MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely +responsible for determining the appropriateness of using and distributing the +Program and assumes all risks associated with its exercise of rights under this +Agreement , including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs or +equipment, and unavailability or interruption of operations.

+ +

6. DISCLAIMER OF LIABILITY

+ +

EXCEPT AS EXPRESSLY SET FORTH IN THIS +AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF +THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGES.

+ +

7. GENERAL

+ +

If any provision of this Agreement is invalid +or unenforceable under applicable law, it shall not affect the validity or +enforceability of the remainder of the terms of this Agreement, and without +further action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable.

+ +

If Recipient institutes patent litigation +against any entity (including a cross-claim or counterclaim in a lawsuit) +alleging that the Program itself (excluding combinations of the Program with +other software or hardware) infringes such Recipient's patent(s), then such +Recipient's rights granted under Section 2(b) shall terminate as of the date +such litigation is filed.

+ +

All Recipient's rights under this Agreement +shall terminate if it fails to comply with any of the material terms or +conditions of this Agreement and does not cure such failure in a reasonable +period of time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use and +distribution of the Program as soon as reasonably practicable. However, +Recipient's obligations under this Agreement and any licenses granted by +Recipient relating to the Program shall continue and survive.

+ +

Everyone is permitted to copy and distribute +copies of this Agreement, but in order to avoid inconsistency the Agreement is +copyrighted and may only be modified in the following manner. The Agreement +Steward reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement Steward has +the right to modify this Agreement. The Eclipse Foundation is the initial +Agreement Steward. The Eclipse Foundation may assign the responsibility to +serve as the Agreement Steward to a suitable separate entity. Each new version +of the Agreement will be given a distinguishing version number. The Program +(including Contributions) may always be distributed subject to the version of +the Agreement under which it was received. In addition, after a new version of +the Agreement is published, Contributor may elect to distribute the Program +(including its Contributions) under the new version. Except as expressly stated +in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to +the intellectual property of any Contributor under this Agreement, whether +expressly, by implication, estoppel or otherwise. All rights in the Program not +expressly granted under this Agreement are reserved.

+ +

This Agreement is governed by the laws of the +State of New York and the intellectual property laws of the United States of +America. No party to this Agreement will bring a legal action under this +Agreement more than one year after the cause of action arose. Each party waives +its rights to a jury trial in any resulting litigation.

+ +

 

+ +
+ + + + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/feature.properties b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/feature.properties new file mode 100644 index 000000000..b4befb7e5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/feature.properties @@ -0,0 +1,82 @@ +# feature.properties +# contains externalized strings for feature.xml +# "%foo" in feature.xml corresponds to the key "foo" in this file +# java.io.Properties file (ISO 8859-1 with "\" escapes) +# This file should be translated. + +# "featureName" property - name of the feature +featureName=Spring IDE Boot Microservices Dash + +# "providerName" property - name of the company that provides the feature +providerName=Spring IDE Developers + +# "updateSiteName" property - label for the update site +updateSiteName=Spring IDE Update Site + +# "description" property - description of the feature +description=\ +This feature provides a View for working with Spring Boot Microservices + +# "copyright" property - text of the Copyright +copyright=\ +Copyright (c) 2005, 2012 Spring IDE Developers\n\ +All rights reserved. This program and the accompanying materials\n\ +are made available under the terms of the Eclipse Public License v1.0\n\ +which accompanies this distribution, and is available at\n\ +https://www.eclipse.org/legal/epl-v10.html\n\ +\n\ +Contributors:\n\ + Spring IDE Developers - initial API and implementation\n +################ end of copyright property #################################### + +# "license" property - text of the "Feature Update License" +# should be plain text version of license agreement pointed to be "licenseURL" +license=\ +SPRING IDE PROJECT SOFTWARE USER AGREEMENT\n\ +March 26, 2007\n\ +\n\ +Usage Of Content\n\ +\n\ +THE SPRING IDE PROJECT MAKES AVAILABLE SOFTWARE, DOCUMENTATION, INFORMATION AND/OR OTHER MATERIALS FOR OPEN SOURCE PROJECTS (COLLECTIVELY "CONTENT"). USE OF THE CONTENT IS GOVERNED BY THE TERMS AND CONDITIONS OF THIS AGREEMENT AND/OR THE TERMS AND CONDITIONS OF LICENSE AGREEMENTS OR NOTICES INDICATED OR REFERENCED BELOW. BY USING THE CONTENT, YOU AGREE THAT YOUR USE OF THE CONTENT IS GOVERNED BY THIS AGREEMENT AND/OR THE TERMS AND CONDITIONS OF ANY APPLICABLE LICENSE AGREEMENTS OR NOTICES INDICATED OR REFERENCED BELOW. IF YOU DO NOT AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT AND THE TERMS AND CONDITIONS OF ANY APPLICABLE LICENSE AGREEMENTS OR NOTICES INDICATED OR REFERENCED BELOW, THEN YOU MAY NOT USE THE CONTENT.\n\ +\n\ +Applicable Licenses\n\ +\n\ +Unless otherwise indicated, all Content made available by the Spring IDE Project is provided to you under the terms and conditions of the Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is provided with this Content and is also available at https://www.eclipse.org/legal/epl-v10.html. For purposes of the EPL, "Program" will mean the Content.\n\ +\n\ +Content includes, but is not limited to, source code, object code, documentation and other files maintained in the springsource.org SVN repository ("Repository") in CVS modules ("Modules") and made available as downloadable archives ("Downloads").\n\ +\n\ + - Content may be structured and packaged into modules to facilitate delivering, extending, and upgrading the Content. Typical modules may include plug-ins ("Plug-ins"), plug-in fragments ("Fragments"), and features ("Features").\n\ + - Each Plug-in or Fragment may be packaged as a sub-directory or JAR (Java? ARchive) in a directory named "plugins".\n\ + - A Feature is a bundle of one or more Plug-ins and/or Fragments and associated material. Each Feature may be packaged as a sub-directory in a directory named "features". Within a Feature, files named "feature.xml" may contain a list of the names and version numbers of the Plug-ins and/or Fragments associated with that Feature.\n\ + - Features may also include other Features ("Included Features"). Within a Feature, files named "feature.xml" may contain a list of the names and version numbers of Included Features.\n\ +\n\ +Features may also include other Features ("Included Features"). Files named "feature.xml" may contain a list of the names and version numbers of Included Features.\n\ +\n\ +The terms and conditions governing Plug-ins and Fragments should be contained in files named "about.html" ("Abouts"). The terms and conditions governing Features and Included Features should be contained in files named "license.html" ("Feature Licenses"). Abouts and Feature Licenses may be located in any directory of a Download or Module including, but not limited to the following locations:\n\ +\n\ + - The top-level (root) directory\n\ + - Plug-in and Fragment directories\n\ + - Inside Plug-ins and Fragments packaged as JARs\n\ + - Sub-directories of the directory named "src" of certain Plug-ins\n\ + - Feature directories\n\ +\n\ +Note: if a Feature made available by the Spring IDE project is installed using the Eclipse Update Manager, you must agree to a license ("Feature Update License") during the installation process. If the Feature contains Included Features, the Feature Update License should either provide you with the terms and conditions governing the Included Features or inform you where you can locate them. Feature Update Licenses may be found in the "license" property of files named "feature.properties" found within a Feature. Such Abouts, Feature Licenses, and Feature Update Licenses contain the terms and conditions (or references to such terms and conditions) that govern your use of the associated Content in that directory.\n\ +\n\ +THE ABOUTS, FEATURE LICENSES, AND FEATURE UPDATE LICENSES MAY REFER TO THE EPL OR OTHER LICENSE AGREEMENTS, NOTICES OR TERMS AND CONDITIONS. SOME OF THESE OTHER LICENSE AGREEMENTS MAY INCLUDE (BUT ARE NOT LIMITED TO):\n\ +\n\ + - Eclipse Public License Version 1.0 (available at https://www.eclipse.org/legal/epl-v10.html)\n\ + - Eclipse Distribution License Version 1.0 (available at https://www.eclipse.org/licenses/edl-v1.0.html)\n\ + - Common Public License Version 1.0 (available at https://www.eclipse.org/legal/cpl-v10.html)\n\ + - Apache Software License 1.1 (available at https://www.apache.org/licenses/LICENSE)\n\ + - Apache Software License 2.0 (available at https://www.apache.org/licenses/LICENSE-2.0)\n\ + - Mozilla Public License Version 1.1 (available at https://www.mozilla.org/MPL/MPL-1.1.html)\n\ +\n\ +IT IS YOUR OBLIGATION TO READ AND ACCEPT ALL SUCH TERMS AND CONDITIONS PRIOR TO USE OF THE CONTENT. If no About, Feature License, or Feature Update License is provided, please contact the Spring IDE project to determine what terms and conditions govern that particular Content.\n\ +Cryptography\n\ +\n\ +Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted.\n\ +\n\ +Java and all Java-based trademarks are trademarks of Sun Microsystems, Inc. in the United States, other countries, or both. +########### end of license property ########################################## + +licenseUrl=epl-v10.html diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/feature.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/feature.xml new file mode 100644 index 000000000..fef265233 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/feature.xml @@ -0,0 +1,110 @@ + + + + + + %description + + + + %copyright + + + + %license + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/p2.inf b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/p2.inf new file mode 100644 index 000000000..3be22ebc3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/p2.inf @@ -0,0 +1,3 @@ +#instructions.configure=\ +# org.eclipse.equinox.p2.touchpoint.eclipse.addRepository(type:0,location:https${#58}//download.eclipse.org/releases/something,name:Something);\ +# org.eclipse.equinox.p2.touchpoint.eclipse.uninstallBundle(bundle:org.slf4j.api_1.7.2.v20121108-1250); diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/pom.xml new file mode 100644 index 000000000..1f098ffde --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + + org.springframework.boot.ide + org.springframework.boot.ide + 4.8.1-SNAPSHOT + ../../eclipse-distribution/pom.xml + + + org.springframework.ide.eclipse + org.springframework.ide.eclipse.boot.dash.feature + + eclipse-feature + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + second-generate-p2-metadata + + p2-metadata + + verify + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.classpath new file mode 100644 index 000000000..3e5654f17 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.project new file mode 100644 index 000000000..2e7108723 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.project @@ -0,0 +1,28 @@ + + + org.springframework.ide.eclipse.boot.dash.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..9f6ece88b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.settings/org.eclipse.jdt.ui.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..a15aefcfd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,60 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=false +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=false +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_type_arguments=true +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=false +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true +sp_cleanup.use_type_arguments=false diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/META-INF/MANIFEST.MF b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/META-INF/MANIFEST.MF new file mode 100644 index 000000000..eaf6d387a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/META-INF/MANIFEST.MF @@ -0,0 +1,58 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Test +Bundle-SymbolicName: org.springframework.ide.eclipse.boot.dash.test +Bundle-Version: 4.8.1.qualifier +Bundle-Vendor: Pivotal Inc +Require-Bundle: org.junit, + org.eclipse.core.resources, + org.springsource.ide.eclipse.commons.tests.util, + org.apache.commons.io, + org.eclipse.debug.core, + org.eclipse.jdt.core, + org.springsource.ide.eclipse.commons.livexp, + org.springsource.ide.eclipse.commons.frameworks.core, + org.springsource.ide.eclipse.commons.frameworks.test.util, + org.springframework.ide.eclipse.boot.launch, + org.eclipse.jdt.launching, + org.eclipse.debug.ui, + org.mockito, + org.springframework.ide.eclipse.boot, + org.eclipse.core.expressions, + org.springframework.ide.eclipse.boot.test, + com.google.guava;bundle-version="15.0.0", + org.eclipse.ui, + org.eclipse.equinox.security, + org.reactivestreams.reactive-streams;bundle-version="1.0.0", + io.projectreactor.reactor-core;bundle-version="[3.3.1,3.3.2)", + org.apache.commons.lang, + org.hamcrest;bundle-version="[1.1.0,1.2.0)", + org.eclipse.lsp4e, + org.eclipse.lsp4j, + org.springframework.ide.eclipse.boot.dash;bundle-version="3.9.12", + org.springframework.ide.eclipse.boot.dash.cf;bundle-version="3.9.12", + org.springframework.ide.eclipse.beans.ui.live, + org.eclipse.jface.text, + org.eclipse.ui.editors, + org.apache.commons.lang3, + org.springframework.ide.eclipse.editor.support, + org.eclipse.ui.ide, + org.springsource.ide.eclipse.commons.core, + org.yaml.snakeyaml, + com.google.gson, + org.springsource.ide.eclipse.commons.cloudfoundry.client.v2, + org.springframework.tooling.cloudfoundry.manifest.ls, + org.springframework.tooling.ls.eclipse.commons, + org.springframework.ide.eclipse.boot.dash.docker;bundle-version="3.9.13", + org.springframework.ide.eclipse.buildship30, + org.eclipse.ui.console, + org.springframework.ide.eclipse.docker.client;bundle-version="3.9.14", + org.springsource.ide.eclipse.commons.boot.ls;bundle-version="4.8.1" +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Import-Package: org.eclipse.core.runtime, + org.eclipse.core.runtime.jobs, + org.eclipse.core.runtime.preferences, + org.osgi.framework, + org.osgi.service.prefs;version="1.1.1" +Automatic-Module-Name: org.springframework.ide.eclipse.boot.dash.test diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/build.properties b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/build.properties new file mode 100644 index 000000000..c25492030 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/build.properties @@ -0,0 +1,7 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + mergeTestsData/,\ + manifest-parse-data/,\ + manifest-generate-data/ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/command-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/command-1.yml new file mode 100644 index 000000000..ab1b632d4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/command-1.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 512M + command: my-command + routes: + - route: app.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/health-check-http.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/health-check-http.yml new file mode 100644 index 000000000..0fda92524 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/health-check-http.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 512M + health-check-type: http + health-check-http-endpoint: /testhealth + routes: + - route: app.springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/health-check-port.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/health-check-port.yml new file mode 100644 index 000000000..3b9d2f164 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/health-check-port.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 512M + health-check-type: port + routes: + - route: app.springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/health-check-process.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/health-check-process.yml new file mode 100644 index 000000000..67f1b2f15 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/health-check-process.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 512M + health-check-type: process + routes: + - route: app.springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/no-hostname-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/no-hostname-1.yml new file mode 100644 index 000000000..774f72f81 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/no-hostname-1.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 512M + routes: + - route: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/no-hostname-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/no-hostname-2.yml new file mode 100644 index 000000000..08af2041e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/no-hostname-2.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 512M + routes: + - route: springsource.org + - route: spring.io diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/no-route-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/no-route-1.yml new file mode 100644 index 000000000..9ae3e35df --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/no-route-1.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: app + memory: 512M + no-route: true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/routes-no-route.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/routes-no-route.yml new file mode 100644 index 000000000..3b51528f4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/routes-no-route.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + no-route: true + routes: + - route: privateapps.io \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/stack-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/stack-1.yml new file mode 100644 index 000000000..a79484479 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/stack-1.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 512M + stack: my-stack + routes: + - route: app.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/uri-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/uri-1.yml new file mode 100644 index 000000000..a71699a08 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/uri-1.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 512M + routes: + - route: app.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/uri-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/uri-2.yml new file mode 100644 index 000000000..3c6c62abe --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/uri-2.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 512M + routes: + - route: app-1.springsource.org + - route: app-2.spring.io diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/uri-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/uri-3.yml new file mode 100644 index 000000000..2d4d44587 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-generate-data/uri-3.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 512M + routes: + - route: app-1.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/command-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/command-1.yml new file mode 100644 index 000000000..ccbd7558d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/command-1.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + host: my-app + domain: spring.io + command: mycommand diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-1.yml new file mode 100644 index 000000000..d1bac889a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-1.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: app + host: my-app + domain: spring.io diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-2.yml new file mode 100644 index 000000000..ffbad966b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-2.yml @@ -0,0 +1,4 @@ +--- +applications: +- name: app + host: my-app diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-3.yml new file mode 100644 index 000000000..a9585bf4b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-3.yml @@ -0,0 +1,7 @@ +--- +domain: spring.io +applications: +- name: app + host: my-app + domain: spring.framework + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-4.yml new file mode 100644 index 000000000..4c2ef812f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-4.yml @@ -0,0 +1,5 @@ +--- +domain: spring.io +applications: +- name: app + host: my-app diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-5.yml new file mode 100644 index 000000000..43ff22403 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-5.yml @@ -0,0 +1,8 @@ +--- +domain: spring.io +applications: +- name: app + host: my-app + domain: spring.framework + domains: + - springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-6.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-6.yml new file mode 100644 index 000000000..8a0e0d7be --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-6.yml @@ -0,0 +1,7 @@ +--- +domain: spring.io +applications: +- name: app + host: my-app + domain: spring.what + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-7.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-7.yml new file mode 100644 index 000000000..be57a2bba --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-7.yml @@ -0,0 +1,9 @@ +--- +domains: +- springsource.org +- spring.framework +applications: +- name: app + host: my-app + domains: + - spring.io diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-8.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-8.yml new file mode 100644 index 000000000..08e8b290a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-8.yml @@ -0,0 +1,8 @@ +--- +domain: spring.io +applications: +- name: app + host: my-app + domains: + - springsource.org + - spring.framework \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-9.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-9.yml new file mode 100644 index 000000000..65ee98f38 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/domains-9.yml @@ -0,0 +1,8 @@ +--- +domains: +- springsource.org +- spring.framework +applications: +- name: app + host: my-app + domain: spring.io diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/health-check-http.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/health-check-http.yml new file mode 100644 index 000000000..d83c85dbd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/health-check-http.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + health-check-type: http + health-check-http-endpoint: /testhealth + routes: + - route: my-route.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/health-check-port.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/health-check-port.yml new file mode 100644 index 000000000..39bbda950 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/health-check-port.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + health-check-type: port + routes: + - route: my-route.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/health-check-process.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/health-check-process.yml new file mode 100644 index 000000000..ed1efe05c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/health-check-process.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + health-check-type: process + routes: + - route: my-route.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-1.yml new file mode 100644 index 000000000..95eea7d4c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-1.yml @@ -0,0 +1,3 @@ +--- +applications: +- name: app diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-2.yml new file mode 100644 index 000000000..fce4dbc91 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-2.yml @@ -0,0 +1,4 @@ +--- +host: my-app +applications: +- name: app diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-3.yml new file mode 100644 index 000000000..ffbad966b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-3.yml @@ -0,0 +1,4 @@ +--- +applications: +- name: app + host: my-app diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-4.yml new file mode 100644 index 000000000..6d27e9e23 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-4.yml @@ -0,0 +1,5 @@ +--- +host: my-super-app +applications: +- name: app + host: my-app diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-5.yml new file mode 100644 index 000000000..0d691a9fd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-5.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + host: my-app-1 + hosts: + - my-app-2 + - my-app-3 + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-6.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-6.yml new file mode 100644 index 000000000..4a3f64451 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/hosts-6.yml @@ -0,0 +1,12 @@ +--- +host: my-root-1 +hosts: + - my-root-2 + - my-root-3 +applications: +- name: app + host: my-app-1 + hosts: + - my-app-2 + - my-app-3 + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-1.yml new file mode 100644 index 000000000..a57cd5cca --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-1.yml @@ -0,0 +1,4 @@ +--- +applications: +- name: app + domain: spring.io diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-10.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-10.yml new file mode 100644 index 000000000..5360daa44 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-10.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + domain: spring.io + memory: 4Gb + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-11.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-11.yml new file mode 100644 index 000000000..685c927ba --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-11.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + domain: spring.io + memory: 1500MB + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-2.yml new file mode 100644 index 000000000..e845624a0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-2.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + domain: spring.io + memory: 768 + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-3.yml new file mode 100644 index 000000000..8faeb51ab --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-3.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + domain: spring.io + memory: 768m + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-4.yml new file mode 100644 index 000000000..86ad28278 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-4.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + domain: spring.io + memory: 768Mb + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-5.yml new file mode 100644 index 000000000..63bd01444 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-5.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + domain: spring.io + memory: 1G + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-6.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-6.yml new file mode 100644 index 000000000..e63a7a1f7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-6.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + domain: spring.io + memory: 1gB + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-7.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-7.yml new file mode 100644 index 000000000..6a98625f2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-7.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + domain: spring.io + memory: 1 G + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-8.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-8.yml new file mode 100644 index 000000000..a1bd04e72 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-8.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + domain: spring.io + memory: 1.5G + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-9.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-9.yml new file mode 100644 index 000000000..7d95aa53c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/memory-9.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + domain: spring.io + memory: 3G + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-1.yml new file mode 100644 index 000000000..2cf0cf00f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-1.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: app + host: my-app + no-hostname: true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-2.yml new file mode 100644 index 000000000..862775e0f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-2.yml @@ -0,0 +1,5 @@ +--- +host: my-app +applications: +- name: app + no-hostname: true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-3.yml new file mode 100644 index 000000000..3a7e0c8a5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-3.yml @@ -0,0 +1,5 @@ +--- +no-hostname: true +applications: +- name: app + host: my-app diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-4.yml new file mode 100644 index 000000000..3058e7f65 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-4.yml @@ -0,0 +1,4 @@ +--- +no-hostname: true +applications: +- name: app diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-5.yml new file mode 100644 index 000000000..a05c75f56 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-5.yml @@ -0,0 +1,4 @@ +--- +applications: +- name: app + no-hostname: true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-6.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-6.yml new file mode 100644 index 000000000..8053a42b4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-hostname-6.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + no-hostname: true + domains: + - springsource.org + - spring.framework \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-1.yml new file mode 100644 index 000000000..25e936873 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-1.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + no-route: true + host: my-app + domains: + - springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-2.yml new file mode 100644 index 000000000..a60caad02 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-2.yml @@ -0,0 +1,6 @@ +--- +host: my-app +domain: springsource.org +applications: +- name: app + no-route: true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-3.yml new file mode 100644 index 000000000..288935429 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-3.yml @@ -0,0 +1,7 @@ +--- +no-route: true +applications: +- name: app + host: my-app + random-route: true + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-4.yml new file mode 100644 index 000000000..db2918e94 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-4.yml @@ -0,0 +1,6 @@ +--- +no-route: true +applications: +- name: app + no-hostname: true + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-5.yml new file mode 100644 index 000000000..2f20c079b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/no-route-5.yml @@ -0,0 +1,6 @@ +--- +no-route: true +applications: +- name: app + routes: + route: app.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-1.yml new file mode 100644 index 000000000..bb8ff5d93 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-1.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: app + host: my-app + random-route: true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-2.yml new file mode 100644 index 000000000..48448d2d7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-2.yml @@ -0,0 +1,5 @@ +--- +host: my-app +applications: +- name: app + random-route: true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-3.yml new file mode 100644 index 000000000..ee53da689 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-3.yml @@ -0,0 +1,4 @@ +--- +applications: +- name: app + random-route: true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-4.yml new file mode 100644 index 000000000..c03276a90 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-4.yml @@ -0,0 +1,4 @@ +--- +random-route: true +applications: +- name: app diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-5.yml new file mode 100644 index 000000000..6ea96023e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/random-route-5.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + random-route: true + domains: + - spring.io + - spring.framework diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/root-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/root-1.yml new file mode 100644 index 000000000..a50daa7e7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/root-1.yml @@ -0,0 +1,3 @@ +--- +name: app +domain: spring.io diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-1.yml new file mode 100644 index 000000000..50f8372c1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-1.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + host: my-app + random-route: true + routes: + - route: my-route.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-2.yml new file mode 100644 index 000000000..01dc95880 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-2.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + host: my-app + routes: + - route: my-route-1.springsource.org + - route: my-route-2.springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-3.yml new file mode 100644 index 000000000..75478bb4c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-3.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + host: my-app + routes: + - garbage + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-4.yml new file mode 100644 index 000000000..19925d8e5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-4.yml @@ -0,0 +1,9 @@ +routes: +- route: my-route-1.springsource.org +- route: my-route-2.springsource.org +applications: +- name: app + hosts: + - aboyko-test1 + path: target/demo-0.0.1-SNAPSHOT.jar + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-5.yml new file mode 100644 index 000000000..616c2c4bf --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-5.yml @@ -0,0 +1,11 @@ +routes: +- route: my-root-route-1.springsource.org +- route: my-route-2.springsource.org +applications: +- name: app + hosts: + - aboyko-test1 + routes: + - route: my-route-1.springsource.org + - route: my-route-2.springsource.org + path: target/demo-0.0.1-SNAPSHOT.jar diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-6.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-6.yml new file mode 100644 index 000000000..b01ebc493 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/routes-6.yml @@ -0,0 +1,9 @@ +applications: +- name: app + hosts: + - aboyko-test1 + routes: + - route: my-route-1.invaliddomain.org + - route: my-route-2.springsource.org + path: target/demo-0.0.1-SNAPSHOT.jar + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/stack-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/stack-1.yml new file mode 100644 index 000000000..b6578c426 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/stack-1.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + host: my-app + domain: spring.io + stack: stack1 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/uris-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/uris-1.yml new file mode 100644 index 000000000..0aa135629 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/manifest-parse-data/uris-1.yml @@ -0,0 +1,12 @@ +--- +applications: +- name: app + host: my-app-1 + hosts: + - my-app-2 + - my-app-3 + domain: springsource.org + domains: + - spring.io + - spring.framework + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-1-expected.yml new file mode 100644 index 000000000..1c6140399 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-1-expected.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 1500 + host: test-app + domain: springsource.org +- name: app1 + memory: 2048M + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-1.yml new file mode 100644 index 000000000..82b70813f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-1.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 1500 + host: test-app + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-2-expected.yml new file mode 100644 index 000000000..4f4566033 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-2-expected.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 1500 + host: test-app + domain: springsource.org +- name: app1 + memory: 2048M + host: test-app-1 + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-2.yml new file mode 100644 index 000000000..2e0edcc1a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-2.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 1500 + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-3-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-3-expected.yml new file mode 100644 index 000000000..b5aa0c88b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-3-expected.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 1500 + routes: + - route: test-app.springsource.org +- name: app1 + memory: 2048M + routes: + - route: test-app-1.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-3.yml new file mode 100644 index 000000000..c780ad674 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-3.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 1500 + routes: + - route: test-app.springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-4-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-4-expected.yml new file mode 100644 index 000000000..2fd5673ef --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-4-expected.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 1500 +- name: app1 + memory: 2048M + routes: + - route: app-1.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-4.yml new file mode 100644 index 000000000..09222c57d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/appNameNoMatch-4.yml @@ -0,0 +1,4 @@ +--- +applications: +- name: app + memory: 1500 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-http-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-http-expected.yml new file mode 100644 index 000000000..2e8c211e4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-http-expected.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + health-check-http-endpoint: /testhealth + memory: 2048M + health-check-type: http + routes: + - route: app-1.springsource.org + + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-http.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-http.yml new file mode 100644 index 000000000..c107cc1ca --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-http.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 2048M + health-check-type: process + routes: + - route: app-1.springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-port-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-port-expected.yml new file mode 100644 index 000000000..070112b7d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-port-expected.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 2048M + routes: + - route: app-1.springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-port.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-port.yml new file mode 100644 index 000000000..c107cc1ca --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-port.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 2048M + health-check-type: process + routes: + - route: app-1.springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-process-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-process-expected.yml new file mode 100644 index 000000000..c107cc1ca --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-process-expected.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 2048M + health-check-type: process + routes: + - route: app-1.springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-process.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-process.yml new file mode 100644 index 000000000..6b478235b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/health-check-process.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: app + memory: 2048M + health-check-type: http + health-check-http-endpoint: /testhealth + routes: + - route: app-1.springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-1-expected.yml new file mode 100644 index 000000000..a800b77ef --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-1-expected.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + host: my-app + domain: springsource.org + memory: 2048M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-1.yml new file mode 100644 index 000000000..fbb9b2dac --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-1.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: app + memory: 2048M + no-hostname: true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-2-expected.yml new file mode 100644 index 000000000..2e5a126d7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-2-expected.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + hosts: + - app1 + - app2 + domain: springsource.org + memory: 2048M + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-2.yml new file mode 100644 index 000000000..b33572ca3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-2.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + host: app-1 + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-3.yml new file mode 100644 index 000000000..47d46fcbd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-3.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 2048M + host: app1 + hosts: + - app2 + domain: springsource.org + domains: + - spring.io diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-4-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-4-expected.yml new file mode 100644 index 000000000..b2a0420de --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-4-expected.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 2048M + host: app1 + hosts: + - app2 + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-4.yml new file mode 100644 index 000000000..a29770484 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-4.yml @@ -0,0 +1,11 @@ +--- +applications: +- name: app + memory: 2048M + host: app1 + hosts: + - app2 + domain: springsource.org + domains: + - spring.io + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-5-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-5-expected.yml new file mode 100644 index 000000000..fd8660e15 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-5-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + host: app + domain: springsource.org + memory: 2048M diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-5.yml new file mode 100644 index 000000000..ef310e52c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-5.yml @@ -0,0 +1,11 @@ +--- +applications: +- name: app + memory: 2048M + hosts: + - app1 + - app2 + domains: + - spring.io + - springsource.org + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-6-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-6-expected.yml new file mode 100644 index 000000000..6c7a7a3b6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-6-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + no-hostname: true + domain: spring.framework + memory: 2048M diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-6.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-6.yml new file mode 100644 index 000000000..ef310e52c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-6.yml @@ -0,0 +1,11 @@ +--- +applications: +- name: app + memory: 2048M + hosts: + - app1 + - app2 + domains: + - spring.io + - springsource.org + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-7-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-7-expected.yml new file mode 100644 index 000000000..6c7a7a3b6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-7-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + no-hostname: true + domain: spring.framework + memory: 2048M diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-7.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-7.yml new file mode 100644 index 000000000..9fa04061b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-7.yml @@ -0,0 +1,13 @@ +--- +applications: +- name: app + memory: 2048M + host: app3 + hosts: + - app1 + - app2 + domains: + - spring.io + - springsource.org + domain: spring.framework + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-8-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-8-expected.yml new file mode 100644 index 000000000..269d8456d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-8-expected.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: app + memory: 2048M + hosts: + - app1 + - app2 + domain: spring.io + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-8.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-8.yml new file mode 100644 index 000000000..af50295b3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/hosts-domains-8.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 2048M + hosts: + - app1 + - app2 + - app3 + domain: spring.io + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-1-expected.yml new file mode 100644 index 000000000..2c16df743 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-1-expected.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-1.yml new file mode 100644 index 000000000..74dc9e29e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-1.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + instances: 2 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-2-expected.yml new file mode 100644 index 000000000..15f3467e3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-2-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-2.yml new file mode 100644 index 000000000..438a6231e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-2.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 2048M + instances: 2 + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-3-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-3-expected.yml new file mode 100644 index 000000000..15f3467e3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-3-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-3.yml new file mode 100644 index 000000000..c0438c69d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-3.yml @@ -0,0 +1,7 @@ +--- +applications: +- instances: 2 + name: app + memory: 2048M + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-4.yml new file mode 100644 index 000000000..2d433c570 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-4.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + instances: 1 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-5-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-5-expected.yml new file mode 100644 index 000000000..4e5b69e0d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-5-expected.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + +- name: another-app diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-5.yml new file mode 100644 index 000000000..ed107611d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/instances-5.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + instances: 2 +- name: another-app diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-1-expected.yml new file mode 100644 index 000000000..595266879 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-1-expected.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + env: + KEY1: value1 + KEY2: value2 + KEY3: value3 + memory: 2048M + host: test-app + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-1.yml new file mode 100644 index 000000000..1f6f740b4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-1.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-10-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-10-expected.yml new file mode 100644 index 000000000..0b8d8d5dd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-10-expected.yml @@ -0,0 +1,11 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + env: + KEY2: value2 + KEY1: value1 + KEY3: value3 + KEY4: value4 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-10.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-10.yml new file mode 100644 index 000000000..a37c4ccf3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-10.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + env: + KEY1-DELETE: value1 + KEY3-DELETE: value3 + KEY2: value2 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-11-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-11-expected.yml new file mode 100644 index 000000000..d27af4cf8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-11-expected.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 2048M + env: + KEY1: value1 + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-11.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-11.yml new file mode 100644 index 000000000..e1158e570 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-11.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 2048M + env: + KEY1: value1 + KEY1-DELETE: value1 + KEY2-DELETE: value2 + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-2-expected.yml new file mode 100644 index 000000000..adc00175d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-2-expected.yml @@ -0,0 +1,11 @@ +--- +applications: +- name: app + env: + KEY1: value1 + KEY2: value2 + KEY3: value3 + memory: 2048M + host: test-app + domain: springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-2.yml new file mode 100644 index 000000000..2c16df743 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-2.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-3-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-3-expected.yml new file mode 100644 index 000000000..2c16df743 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-3-expected.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-3.yml new file mode 100644 index 000000000..30338636f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-3.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + env: + KEY1: value1 + KEY2: value2 + KEY3: value3 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-4-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-4-expected.yml new file mode 100644 index 000000000..15f3467e3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-4-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-4.yml new file mode 100644 index 000000000..eed05d0d0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-4.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 2048M + env: + KEY1: value1 + KEY2: value2 + KEY3: value3 + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-5-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-5-expected.yml new file mode 100644 index 000000000..ebadf7719 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-5-expected.yml @@ -0,0 +1,11 @@ +--- +applications: +- name: app + memory: 2048M + env: + KEY2: value2 + KEY1: value1 + KEY3: value3 + KEY4: value4 + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-5.yml new file mode 100644 index 000000000..9adcef0b3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-5.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 2048M + env: + KEY1-DELETE: value1 + KEY2: value2 + KEY3-DELETE: value3 + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-6-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-6-expected.yml new file mode 100644 index 000000000..6721e22ab --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-6-expected.yml @@ -0,0 +1,13 @@ +--- +applications: +- name: app + memory: 2048M + env: +# Comment 1 + KEY2: value2 + KEY1: value1 + KEY3: value3 + KEY4: value4 +# Comment 2 + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-6.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-6.yml new file mode 100644 index 000000000..732b41117 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-6.yml @@ -0,0 +1,12 @@ +--- +applications: +- name: app + memory: 2048M + env: +# Comment 1 + KEY1-DELETE: value1 + KEY2: value2 +# Comment 2 + KEY3-DELETE: value3 + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-7-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-7-expected.yml new file mode 100644 index 000000000..5a0e1a01f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-7-expected.yml @@ -0,0 +1,19 @@ +--- +applications: +- name: app + memory: 2048M + env: + + +# Comment 1 + + KEY2: value2 + KEY1: value1 + KEY3: value3 + KEY4: value4 + +# Comment 2 + + + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-7.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-7.yml new file mode 100644 index 000000000..d09d4e38b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-7.yml @@ -0,0 +1,18 @@ +--- +applications: +- name: app + memory: 2048M + env: + + +# Comment 1 + + KEY1-DELETE: value1 + KEY2: value2 + +# Comment 2 + + KEY3-DELETE: value3 + + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-8-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-8-expected.yml new file mode 100644 index 000000000..2885d8d44 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-8-expected.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app +# Comment 4 + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-8.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-8.yml new file mode 100644 index 000000000..be3075ebe --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-8.yml @@ -0,0 +1,19 @@ +--- +applications: +- name: app + memory: 2048M + env: + + +# Comment 1 + + KEY1-DELETE: value1 + KEY2-DELETE: value2 + +# Comment 2 + + KEY3-DELETE: value3 +# Comment 3 + host: test-app +# Comment 4 + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-9-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-9-expected.yml new file mode 100644 index 000000000..0b8d8d5dd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-9-expected.yml @@ -0,0 +1,11 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + env: + KEY2: value2 + KEY1: value1 + KEY3: value3 + KEY4: value4 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-9.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-9.yml new file mode 100644 index 000000000..bd0ad96a4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/map-9.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org + env: + KEY1-DELETE: value1 + KEY3-DELETE: value3 + KEY2: value2 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-1-expected.yml new file mode 100644 index 000000000..1f6f740b4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-1-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-1.yml new file mode 100644 index 000000000..82b70813f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-1.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 1500 + host: test-app + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-2-expected.yml new file mode 100644 index 000000000..1f6f740b4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-2-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-2.yml new file mode 100644 index 000000000..054c06578 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-2.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: app + host: test-app + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-3-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-3-expected.yml new file mode 100644 index 000000000..15f3467e3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-3-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-3.yml new file mode 100644 index 000000000..f02e77e45 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-3.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: app + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-4.yml new file mode 100644 index 000000000..04e952ecd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-4.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048m + host: test-app + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-5.yml new file mode 100644 index 000000000..b76e893c5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-5.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2Gb + host: test-app + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-6-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-6-expected.yml new file mode 100644 index 000000000..0eb1d3a8e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-6-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 3000M + host: test-app + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-6.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-6.yml new file mode 100644 index 000000000..000705a65 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/memory-6.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 3Gb + host: test-app + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-1-expected.yml new file mode 100644 index 000000000..1f4de8cdc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-1-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + host: my-app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-1.yml new file mode 100644 index 000000000..634cae770 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-1.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + no-hostname: true + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-2-expected.yml new file mode 100644 index 000000000..96c9ef0f8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-2-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + no-hostname: true + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-2.yml new file mode 100644 index 000000000..75da8fd83 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-2.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-3.yml new file mode 100644 index 000000000..634cae770 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-hostname-3.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + no-hostname: true + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-1-expected.yml new file mode 100644 index 000000000..30377ded9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-1-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + no-route: true + memory: 2048M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-1.yml new file mode 100644 index 000000000..15f3467e3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-1.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-2-expected.yml new file mode 100644 index 000000000..30377ded9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-2-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + no-route: true + memory: 2048M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-2.yml new file mode 100644 index 000000000..f641f0487 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-2.yml @@ -0,0 +1,14 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + random-route: true + no-hostname: true + hosts: + - h1 + - h2 + domain: springsource.org + domains: + - spring.io + - spring.framework diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-3.yml new file mode 100644 index 000000000..3dcc2d204 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-3.yml @@ -0,0 +1,15 @@ +--- +applications: +- name: app + memory: 2048M + host: test-app + random-route: true + no-route: true + no-hostname: true + hosts: + - h1 + - h2 + domain: springsource.org + domains: + - spring.io + - spring.framework diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-5-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-5-expected.yml new file mode 100644 index 000000000..30377ded9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-5-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + no-route: true + memory: 2048M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-5.yml new file mode 100644 index 000000000..f07a05474 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/no-route-5.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 2048M + random-route: true + routes: + - route: app-1.springsource.org + - route: app-2.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-1-expected.yml new file mode 100644 index 000000000..d393b8400 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-1-expected.yml @@ -0,0 +1,4 @@ +name: app +memory: 2048M +host: test-app +domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-1.yml new file mode 100644 index 000000000..db8289a1d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-1.yml @@ -0,0 +1,4 @@ +name: app +memory: 1500 +host: test-app +domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-2-expected.yml new file mode 100644 index 000000000..60071b1b9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-2-expected.yml @@ -0,0 +1,4 @@ +name: app1 +memory: 2048M +host: test-app +domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-2.yml new file mode 100644 index 000000000..db8289a1d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noAppsNode-2.yml @@ -0,0 +1,4 @@ +name: app +memory: 1500 +host: test-app +domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-1-expected.yml new file mode 100644 index 000000000..0dc160a4e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-1-expected.yml @@ -0,0 +1,7 @@ +--- +name: app +memory: 2048M +routes: +- route: test-app.springsource.org +key1: value1 +key2: value2 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-1.yml new file mode 100644 index 000000000..c8f21ed74 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-1.yml @@ -0,0 +1,3 @@ +--- +key1: value1 +key2: value2 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-2-expected.yml new file mode 100644 index 000000000..1dea14f05 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-2-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + routes: + - route: test-app.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-2.yml new file mode 100644 index 000000000..ed97d539c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/noManifest-2.yml @@ -0,0 +1 @@ +--- diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-1.yml new file mode 100644 index 000000000..25f342dc0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-1.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 2048M + random-route: true + domain: springsource.org \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-2-expected.yml new file mode 100644 index 000000000..e4301919a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-2-expected.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + routes: + - route: my-app1.springsource.org + - route: my-app2.springsource.org + memory: 2048M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-2.yml new file mode 100644 index 000000000..c89e44736 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-2.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: app + memory: 2048M + random-route: true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-3-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-3-expected.yml new file mode 100644 index 000000000..787dbf48d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-3-expected.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: app + buildpack: java_buildpack_offline + memory: 1024M + host: app2 + domain: springsource.org + path: target/demo-0.0.1-SNAPSHOT.jar + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-3.yml new file mode 100644 index 000000000..f29a6217a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-3.yml @@ -0,0 +1,15 @@ +--- +applications: +- name: app + random-route: true + no-route: true + memory: 512M + host: my-app2 + hosts: + - app-2-1 + - app-2-2 + domain: springsource.org + path: target/demo-0.0.1-SNAPSHOT.jar + env: + KEY1: value1 + KEY2: value2 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-4.yml new file mode 100644 index 000000000..0c2f693a0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-4.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: app + random-route: true + memory: 512M + routes: + - route: app-1.springsource.org + - route: app-3.springsource.org + - route: app-2.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-5-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-5-expected.yml new file mode 100644 index 000000000..a71699a08 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-5-expected.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: app + memory: 512M + routes: + - route: app.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-5.yml new file mode 100644 index 000000000..76f2fae28 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/random-route-5.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app + random-route: true + memory: 512M + routes: + - route: app-1.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-1-expected.yml new file mode 100644 index 000000000..9111c60d8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-1-expected.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: app +# instances: 8 + memory: 512M + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-1.yml new file mode 100644 index 000000000..bdc461602 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-1.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app +# instances: 8 + instances: 2 + memory: 512M + host: test-app + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-2-expected.yml new file mode 100644 index 000000000..db90830c7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-2-expected.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: app + memory: 512M + host: test-app + domain: springsource.org + + # instances: 8 + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-2.yml new file mode 100644 index 000000000..4780e255c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-2.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 512M + host: test-app + domain: springsource.org + + # instances: 8 + instances: 2 + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-3-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-3-expected.yml new file mode 100644 index 000000000..92f718a16 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-3-expected.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: app + instances: 2 + memory: 512M + host: test-app + domain: springsource.org + + # instances: 8 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-3.yml new file mode 100644 index 000000000..89ce439e1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-3.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 512M + host: test-app + domain: springsource.org + + # instances: 8 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-4-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-4-expected.yml new file mode 100644 index 000000000..7b9f08e66 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-4-expected.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: app + memory: 512M + host: test-app + domain: springsource.org + + # instances: 8 + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-4.yml new file mode 100644 index 000000000..3ccc49754 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-comment-4.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: app + memory: 512M + host: test-app + domain: springsource.org + + # instances: 8 + instances: 2 + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-1-expected.yml new file mode 100644 index 000000000..e32a709dc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-1-expected.yml @@ -0,0 +1,10 @@ +--- +applications: +- name: another-app + host: test-app +- name: app + hosts: + - app1 + - app2 + domain: springsource.org + memory: 2048M diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-1.yml new file mode 100644 index 000000000..7e0674929 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-1.yml @@ -0,0 +1,6 @@ +--- +host: test-app +applications: +- name: another-app +- name: app + memory: 2048M \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-2-expected.yml new file mode 100644 index 000000000..021735b32 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-2-expected.yml @@ -0,0 +1,13 @@ +--- +host: test-app +applications: +- name: another-app + hosts: + - test-app1 + - test-app2 +- name: app + hosts: + - test-app1 + domain: springsource.org + host: my-app + memory: 2048M \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-2.yml new file mode 100644 index 000000000..1ec178cf7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-2.yml @@ -0,0 +1,10 @@ +--- +host: test-app +hosts: +- test-app1 +- test-app2 +applications: +- name: another-app +- name: app + host: my-app + memory: 2048M \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-3-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-3-expected.yml new file mode 100644 index 000000000..11ec36d2a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-3-expected.yml @@ -0,0 +1,13 @@ +--- +applications: +- name: another-app + host: test-app + hosts: + - test-app1 + - test-app2 +- name: app + hosts: + - test-app-1 + - test-app-3 + domain: springsource.org + memory: 2048M \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-3.yml new file mode 100644 index 000000000..2698aebdc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-hosts-domains-3.yml @@ -0,0 +1,10 @@ +--- +host: test-app +hosts: +- test-app1 +- test-app2 +applications: +- name: another-app +- name: app + host: test-app1 + memory: 2048M \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-1-expected.yml new file mode 100644 index 000000000..0c9f8187d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-1-expected.yml @@ -0,0 +1,11 @@ +--- +services: +- s1 +- s2 +applications: +- name: somapp +- name: app + services: + - s3 + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-1.yml new file mode 100644 index 000000000..c552c6515 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-1.yml @@ -0,0 +1,9 @@ +--- +services: +- s1 +- s2 +applications: +- name: somapp +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-2-expected.yml new file mode 100644 index 000000000..30f5bed6f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-2-expected.yml @@ -0,0 +1,13 @@ +--- +services: +- s1 +- s2 +applications: +- name: somapp +- name: app + memory: 2048M + domain: springsource.org + services: + - s3 + - s4 + - s5 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-2.yml new file mode 100644 index 000000000..d3baa852d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-2.yml @@ -0,0 +1,11 @@ +--- +services: +- s1 +- s2 +applications: +- name: somapp +- name: app + memory: 2048M + domain: springsource.org + services: + - s3 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-3-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-3-expected.yml new file mode 100644 index 000000000..6ed353992 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-3-expected.yml @@ -0,0 +1,15 @@ +--- +applications: +- memory: 512M + name: somapp + services: + - s1 + - s2 +- name: app + memory: 2048M + domain: springsource.org + services: + - s3 + - s1 + - s4 + - s5 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-3.yml new file mode 100644 index 000000000..9bc3492ee --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-list-3.yml @@ -0,0 +1,12 @@ +--- +services: +- s1 +- s2 +applications: +- memory: 512M + name: somapp +- name: app + memory: 2048M + domain: springsource.org + services: + - s3 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-1-expected.yml new file mode 100644 index 000000000..aa9510224 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-1-expected.yml @@ -0,0 +1,11 @@ +--- +env: + k1: v1 + k2: v2 +applications: +- name: somapp +- name: app + env: + k3: v3 + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-1.yml new file mode 100644 index 000000000..d074dfe70 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-1.yml @@ -0,0 +1,9 @@ +--- +env: + k1: v1 + k2: v2 +applications: +- name: somapp +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-2-expected.yml new file mode 100644 index 000000000..42f2bac2a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-2-expected.yml @@ -0,0 +1,11 @@ +--- +env: + k1: v1 + k2: v2 +applications: +- name: somapp +- name: app + env: + k2: v2-alt + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-2.yml new file mode 100644 index 000000000..d074dfe70 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-2.yml @@ -0,0 +1,9 @@ +--- +env: + k1: v1 + k2: v2 +applications: +- name: somapp +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-3-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-3-expected.yml new file mode 100644 index 000000000..9d7865318 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-3-expected.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: somapp + env: + k1: v1 + k2: v2 +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-3.yml new file mode 100644 index 000000000..d074dfe70 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-3.yml @@ -0,0 +1,9 @@ +--- +env: + k1: v1 + k2: v2 +applications: +- name: somapp +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-4-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-4-expected.yml new file mode 100644 index 000000000..1fed7ea89 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-4-expected.yml @@ -0,0 +1,12 @@ +--- +applications: +- name: somapp + env: + k1: v1-1 + k3: v3 + k2: v2 +- name: app + env: + k2: v2 + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-4.yml new file mode 100644 index 000000000..2cdf88a6b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-map-4.yml @@ -0,0 +1,12 @@ +--- +env: + k1: v1 + k2: v2 +applications: +- name: somapp + env: + k1: v1-1 + k3: v3 +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-1-expected.yml new file mode 100644 index 000000000..ac8c5ec1e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-1-expected.yml @@ -0,0 +1,8 @@ +--- +instances: 2 +applications: +- name: somapp +- name: app + instances: 3 + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-1.yml new file mode 100644 index 000000000..d49366c9e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-1.yml @@ -0,0 +1,7 @@ +--- +instances: 2 +applications: +- name: somapp +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-2-expected.yml new file mode 100644 index 000000000..1da3e934a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-2-expected.yml @@ -0,0 +1,8 @@ +--- +instances: 2 +applications: +- name: somapp +- name: app + instances: 1 + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-2.yml new file mode 100644 index 000000000..d49366c9e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-node-2.yml @@ -0,0 +1,7 @@ +--- +instances: 2 +applications: +- name: somapp +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-1-expected.yml new file mode 100644 index 000000000..b22d5b6c8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-1-expected.yml @@ -0,0 +1,7 @@ +--- +no-hostname: true +applications: +- name: app + host: my-app + domain: springsource.org + memory: 2048M diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-1.yml new file mode 100644 index 000000000..7fd4efffe --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-1.yml @@ -0,0 +1,5 @@ +--- +no-hostname: true +applications: +- name: app + memory: 2048M diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-2-expected.yml new file mode 100644 index 000000000..3fe016679 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-2-expected.yml @@ -0,0 +1,8 @@ +--- +no-hostname: true +applications: +- name: app + domains: + - spring.io + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-2.yml new file mode 100644 index 000000000..dbad592c4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-2.yml @@ -0,0 +1,6 @@ +--- +no-hostname: true +applications: +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-3.yml new file mode 100644 index 000000000..dbad592c4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-nohost-3.yml @@ -0,0 +1,6 @@ +--- +no-hostname: true +applications: +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-1-expected.yml new file mode 100644 index 000000000..a9720e700 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-1-expected.yml @@ -0,0 +1,7 @@ +--- +no-route: true +applications: +- name: app + no-route: false + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-1.yml new file mode 100644 index 000000000..cfa5021cc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-1.yml @@ -0,0 +1,6 @@ +--- +no-route: true +applications: +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-2-expected.yml new file mode 100644 index 000000000..b5cb3498b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-2-expected.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: another-app + host: my-app +- name: app + no-route: true + memory: 2048M diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-2.yml new file mode 100644 index 000000000..bf47adccf --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-noroute-2.yml @@ -0,0 +1,7 @@ +--- +host: my-app +applications: +- name: another-app +- name: app + memory: 2048M + domain: springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-randomroute-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-randomroute-1-expected.yml new file mode 100644 index 000000000..aa869ce42 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-randomroute-1-expected.yml @@ -0,0 +1,10 @@ +--- +random-route: true +applications: +- name: another-app +- name: app + routes: + - route: app-1.springsource.org + - route: app-2.springsource.org + memory: 2048M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-randomroute-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-randomroute-1.yml new file mode 100644 index 000000000..68bd30771 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-randomroute-1.yml @@ -0,0 +1,6 @@ +--- +random-route: true +applications: +- name: another-app +- name: app + memory: 2048M diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-randomroute-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-randomroute-2.yml new file mode 100644 index 000000000..68bd30771 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/root-randomroute-2.yml @@ -0,0 +1,6 @@ +--- +random-route: true +applications: +- name: another-app +- name: app + memory: 2048M diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-1-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-1-expected.yml new file mode 100644 index 000000000..88af64d20 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-1-expected.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: app + memory: 2048M + routes: + - route: app-1.springsource.org + - route: app-3.springsource.org + - route: app-2.springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-1.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-1.yml new file mode 100644 index 000000000..1c5f309e1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-1.yml @@ -0,0 +1,8 @@ +--- +applications: +- name: app + memory: 2048M + routes: + - route: app-1.springsource.org + - route: app-2.invaliddomain.org + - route: app-3.springsource.org diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-2-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-2-expected.yml new file mode 100644 index 000000000..88af64d20 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-2-expected.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: app + memory: 2048M + routes: + - route: app-1.springsource.org + - route: app-3.springsource.org + - route: app-2.springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-2.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-2.yml new file mode 100644 index 000000000..37745e199 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-2.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: app + memory: 2048M + routes: + - route: app-1.springsource.org + - route: app-3.springsource.org + - route: app-2.invaliddomain.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-3-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-3-expected.yml new file mode 100644 index 000000000..ca7a80f36 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-3-expected.yml @@ -0,0 +1,10 @@ +--- +routes: +- route: app-1.springsource.org +- route: app-3.springsource.org +applications: +- name: app + routes: + - route: app-2.springsource.org + memory: 2048M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-3.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-3.yml new file mode 100644 index 000000000..c592d8fa3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-3.yml @@ -0,0 +1,10 @@ +--- +routes: +- route: app-1.springsource.org +- route: app-3.springsource.org +applications: +- name: app + routes: + - route: app-2.invaliddomain.org + memory: 2048M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-4-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-4-expected.yml new file mode 100644 index 000000000..0afd80e3d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-4-expected.yml @@ -0,0 +1,9 @@ +--- +applications: +- name: app + routes: + - route: app-1.springsource.org + - route: app-2.springsource.org + - route: app-3.springsource.org + memory: 2048M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-4.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-4.yml new file mode 100644 index 000000000..fa17eba11 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-4.yml @@ -0,0 +1,9 @@ +--- +routes: +- route: app-1.springsource.org +- route: app-2.invaliddomain.org +- route: app-3.springsource.org +applications: +- name: app + memory: 2048M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-5-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-5-expected.yml new file mode 100644 index 000000000..992094563 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-5-expected.yml @@ -0,0 +1,10 @@ +--- +routes: +- route: app-1.springsource.org +applications: +- name: app + memory: 2048M + routes: + - route: app-3.springsource.org + - route: app-2.springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-5.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-5.yml new file mode 100644 index 000000000..307250634 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-5.yml @@ -0,0 +1,11 @@ +--- +routes: +- route: app-1.springsource.org +applications: +- name: app + memory: 2048M + routes: + - route: app-2.invaliddomain.org + - route: app-3.springsource.org + - route: app-notexists.springsource.org + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-6-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-6-expected.yml new file mode 100644 index 000000000..d74fba375 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-6-expected.yml @@ -0,0 +1,7 @@ +applications: +- name: demo-app + routes: + - route: route-2.springsource.org +- name: cvcvcv + memory: 512M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-6.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-6.yml new file mode 100644 index 000000000..47f90b35f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-6.yml @@ -0,0 +1,8 @@ +applications: +- name: demo-app + routes: + - route: route-2.springsource.org + - route: route-1.springsource.org +- name: cvcvcv + memory: 512M + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-paths-ports-expected.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-paths-ports-expected.yml new file mode 100644 index 000000000..1f985d889 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-paths-ports-expected.yml @@ -0,0 +1,11 @@ +--- +routes: +- route: app-1.springsource.org +applications: +- name: app + memory: 2048M + routes: + - route: app-3.springsource.org + - route: app-2.springsource.org + - route: app-4.springsource.org/myappPath/moresegments + - route: tcp.springsource.org:9003 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-paths-ports.yml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-paths-ports.yml new file mode 100644 index 000000000..169f2115a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/mergeTestsData/routes-paths-ports.yml @@ -0,0 +1,14 @@ +--- +routes: +- route: app-1.springsource.org +applications: +- name: app + memory: 2048M + routes: + - route: app-3.springsource.org + - route: app-2.springsource.org + - route: app-notexists.springsource.org + - route: app-4.springsource.org/myappPath/moresegments + - route: app-4.springsource.org/invalidPath + - route: tcp.springsource.org:9003 + - route: tcp.springsource.org:5555 \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/pom.xml new file mode 100644 index 000000000..2c5465219 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + org.springframework.boot.ide + org.springframework.boot.ide + 4.8.1-SNAPSHOT + ../../eclipse-distribution/pom.xml + + + org.springframework.ide.eclipse + org.springframework.ide.eclipse.boot.dash.test + eclipse-test-plugin + + + + sts4-language-servers-snapshot + p2 + https://dist.springsource.com/snapshot/TOOLS/sts4-language-servers/nightly/ + + + + + + + org.eclipse.tycho + tycho-surefire-plugin + ${tycho-version} + + true + 7200 + org.springframework.ide.eclipse.boot.dash.test + org.springframework.ide.eclipse.boot.dash.test.AllBootDashTests + + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + + + eclipse-plugin + org.eclipse.equinox.event + 0.0.0 + + + eclipse-plugin + org.apache.felix.scr + 0.0.0 + + + eclipse-feature + org.eclipse.m2e.feature + 0.0.0 + + + eclipse-feature + org.springframework.tooling.cloudfoundry.manifest.ls.feature + 0.0.0 + + + eclipse-plugin + org.springframework.ide.eclipse.boot.dash.cf + 0.0.0 + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/AbstractLaunchConfigurationsDashElementTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/AbstractLaunchConfigurationsDashElementTest.java new file mode 100644 index 000000000..2f91a152d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/AbstractLaunchConfigurationsDashElementTest.java @@ -0,0 +1,355 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test; + +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.stringContainsInOrder; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.ui.IDebugUIConstants; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.hamcrest.Matcher; +import org.junit.Test; +import org.mockito.hamcrest.MockitoHamcrest; +import org.springframework.ide.eclipse.boot.dash.model.AbstractLaunchConfigurationsDashElement; +import org.springframework.ide.eclipse.boot.dash.model.LocalBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.LocalRunTarget; +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.ngrok.NGROKClient; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKLaunchTracker; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKTunnel; +import org.springframework.ide.eclipse.boot.dash.test.mocks.Mocks; +import org.springframework.ide.eclipse.boot.dash.util.LaunchConfRunStateTracker; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.InMemoryPropertyStore; + +import com.google.common.collect.ImmutableSet; + + +/** + * Unit tests some of the methods that don't have great coverage yet + * after running BootDashModelTests. These methods are harder to test + * from the model (which does only minimal mocking). + * + * We perform focussed tests here using Mockito to essentially just test + * those methods in isolation here using mocks for anything it uses. + * + * @author Kris De Volder + */ +public class AbstractLaunchConfigurationsDashElementTest extends Mocks { + + public static class TestElement extends AbstractLaunchConfigurationsDashElement { + + private IProject project; + + public TestElement(String delegate, IProject project, LocalBootDashModel context) { + super(context, delegate); + this.project = project; + } + + @Override + public void launch(String runMode, ILaunchConfiguration conf) { + } + + @Override + public IType[] guessMainTypes() throws CoreException { + return NO_TYPES; + } + + @Override + protected IPropertyStore createPropertyStore() { + return new InMemoryPropertyStore(); + } + + @Override + public ImmutableSet getLaunchConfigs() { + return ImmutableSet.of(); + } + + @Override + public IProject getProject() { + return project; + } + + @Override + public String getName() { + return delegate; + } + + @Override + public ImmutableSet getLaunches() { + return ImmutableSet.of(); + } + + @Override + public Object getParent() { + return getBootDashModel(); + } + } + + private static final IType[] NO_TYPES = {}; + + public static TestElement createElement(String testElementName, IProject project, IJavaProject javaProject, LocalRunTarget runTarget) { +// BootProjectDashElementFactory factory = mock(BootProjectDashElementFactory.class); +// LaunchConfDashElementFactory childFactory = mock(LaunchConfDashElementFactory.class); +// LaunchConfRunStateTracker runStateTracker = mock(LaunchConfRunStateTracker.class); +// when(model.getLaunchConfRunStateTracker()).thenReturn(runStateTracker); +// IProject project = javaProject.getProject(); + + LocalBootDashModel model = mock(LocalBootDashModel.class); + LaunchConfRunStateTracker runStateTracker = mock(LaunchConfRunStateTracker.class); + when(model.getLaunchConfRunStateTracker()).thenReturn(runStateTracker); + TestElement element = spy(new TestElement(testElementName, project, model)); + when(element.getTarget()).thenReturn(runTarget); + doReturn(javaProject).when(element).getJavaProject(); + return element; + } + + public static IType mockType(IJavaProject javaProject, String pkg, String name) { + IType type = mock(IType.class); + when(type.getElementName()).thenReturn(name); + when(type.getFullyQualifiedName()).thenReturn(pkg+"."+name); + when(type.getJavaProject()).thenReturn(javaProject); + return type; + } + + @Test(expected=IllegalArgumentException.class) + public void restartWithBadArgument() throws Exception { + String projectName = "fooProject"; + IProject project = mockProject(projectName, true); + IJavaProject javaProject = mockJavaProject(project); + LocalRunTarget runTarget = mock(LocalRunTarget.class); + TestElement element = createElement(projectName, project, javaProject, runTarget); + UserInteractions ui = mock(UserInteractions.class); + + element.restart(RunState.INACTIVE, ui); + } + + public static IJavaProject mockJavaProject(IProject project) { + String projectName = project.getName(); + IJavaProject jp = mock(IJavaProject.class); + when(jp.getElementName()).thenReturn(projectName); + when(jp.getProject()).thenReturn(project); + return jp; + } + + @Test + public void restartNoMainTypes() throws Exception { + String projectName = "fooProject"; + IProject project = mockProject(projectName, true); + IJavaProject javaProject = mockJavaProject(project); + LocalRunTarget runTarget = mock(LocalRunTarget.class); + TestElement element = createElement(projectName, project, javaProject, runTarget); + UserInteractions ui = mock(UserInteractions.class); + + when(element.guessMainTypes()).thenReturn(NO_TYPES); + + element.restart(RunState.RUNNING, ui); + + verify(element).stopSync(); + verify(ui).errorPopup( + stringContains("Problem"), + stringContains("Couldn't find a main type") + ); + verifyNoMoreInteractions(ui); + } + + @Test + public void restartOneMainType() throws Exception { + String projectName = "fooProject"; + IProject project = mockProject(projectName, true); + IJavaProject javaProject = mockJavaProject(project); + LocalRunTarget runTarget = mock(LocalRunTarget.class); + TestElement element = createElement(projectName, project, javaProject, runTarget); + UserInteractions ui = mock(UserInteractions.class); + ILaunchConfiguration conf = mock(ILaunchConfiguration.class); + IType type = mockType(javaProject, "demo", "FooApplication"); + + when(element.guessMainTypes()).thenReturn(new IType[] {type}); + when(runTarget.createLaunchConfig(javaProject, type)).thenReturn(conf); + + element.restart(RunState.RUNNING, ui); + + verify(element).stopSync(); + verify(element).launch(ILaunchManager.RUN_MODE, conf); + verifyZeroInteractions(ui); + } + + @Test + public void restartAndExpose() throws Exception { + String projectName = "fooProject"; + IProject project = mockProject(projectName, true); + IJavaProject javaProject = mockJavaProject(project); + LocalRunTarget runTarget = mock(LocalRunTarget.class); + TestElement element = createElement(projectName, project, javaProject, runTarget); + UserInteractions ui = mock(UserInteractions.class); + ILaunchConfiguration conf = mock(ILaunchConfiguration.class); + ILaunchConfigurationWorkingCopy copyOfConf = mock(ILaunchConfigurationWorkingCopy.class); + IType type = mockType(javaProject, "demo", "FooApplication"); + NGROKClient ngrokClient = mock(NGROKClient.class); + NGROKTunnel tunnel = new NGROKTunnel("foo-launch", "http", "publicURLTest", "8888"); + + when(element.guessMainTypes()).thenReturn(new IType[] {type}); + when(runTarget.createLaunchConfig(javaProject, type)).thenReturn(conf); + when(conf.getWorkingCopy()).thenReturn(copyOfConf); + when(element.getLivePort()).thenReturn(8888); + when(ngrokClient.startTunnel("http", "8888")).thenReturn(tunnel); + when(conf.getName()).thenReturn("foo-launch"); + when(copyOfConf.getName()).thenReturn("foo-launch"); + String eurekaInstance = "eureka instance somewhere"; + + element.restartAndExpose(RunState.RUNNING, ngrokClient, eurekaInstance, ui); + + verify(element).stopSync(); + verify(element).launch(ILaunchManager.RUN_MODE, copyOfConf); + verifyZeroInteractions(ui); + + verify(copyOfConf).setAttribute("spring.boot.prop.server.port", "18888"); + verify(copyOfConf).setAttribute("spring.boot.prop.eureka.instance.hostname", "1publicURLTest"); + verify(copyOfConf).setAttribute("spring.boot.prop.eureka.instance.nonSecurePort", "180"); + verify(copyOfConf).setAttribute("spring.boot.prop.eureka.client.service-url.defaultZone", "1" + eurekaInstance); + + NGROKClient storedNgrokClient = NGROKLaunchTracker.get("foo-launch"); + assertEquals(storedNgrokClient, ngrokClient); + NGROKLaunchTracker.remove("foo-launch"); + } + + @Test + public void restartTwoMainTypes() throws Exception { + String projectName = "fooProject"; + IProject project = mockProject(projectName, true); + IJavaProject javaProject = mockJavaProject(project); + LocalRunTarget runTarget = mock(LocalRunTarget.class); + TestElement element = createElement(projectName, project, javaProject, runTarget); + UserInteractions ui = mock(UserInteractions.class); + ILaunchConfiguration conf = mock(ILaunchConfiguration.class); + IType fooType = mockType(javaProject, "demo", "FooApplication"); + IType barType = mockType(javaProject, "demo", "BarApplication"); + + when(element.guessMainTypes()).thenReturn(new IType[] {fooType, barType}); + when(ui.chooseMainType( + MockitoHamcrest.argThat(arrayContaining(fooType, barType)), + any(String.class), + any(String.class) + )).thenReturn(barType); + when(runTarget.createLaunchConfig(javaProject, barType)).thenReturn(conf); + + element.restart(RunState.RUNNING, ui); + + verify(element).stopSync(); + verify(ui).chooseMainType(any(IType[].class), + stringContains("Choose"), + stringContains("Choose", projectName) + ); + verify(element).launch(ILaunchManager.RUN_MODE, conf); + verifyNoMoreInteractions(ui); + } + + @Test + public void openConfigWithNoExistingConfs() throws Exception { + String projectName = "fooProject"; + IProject project = mockProject(projectName, true); + IJavaProject javaProject = mockJavaProject(project); + LocalRunTarget runTarget = mock(LocalRunTarget.class); + TestElement element = createElement(projectName, project, javaProject, runTarget); + UserInteractions ui = mock(UserInteractions.class); + ILaunchConfiguration conf = mock(ILaunchConfiguration.class); + + when(runTarget.createLaunchConfig(javaProject, null)).thenReturn(conf); + doReturn(RunState.INACTIVE).when(element).getRunState(); + + element.openConfig(ui); + + verify(ui).openLaunchConfigurationDialogOnGroup(conf, IDebugUIConstants.ID_DEBUG_LAUNCH_GROUP); + verifyNoMoreInteractions(ui); + } + + @Test + public void openConfigWithOneExistingConfs() throws Exception { + String projectName = "fooProject"; + IProject project = mockProject(projectName, true); + IJavaProject javaProject = mockJavaProject(project); + LocalRunTarget runTarget = mock(LocalRunTarget.class); + TestElement element = createElement(projectName, project, javaProject, runTarget); + UserInteractions ui = mock(UserInteractions.class); + ILaunchConfiguration conf = mock(ILaunchConfiguration.class); + + when(element.getLaunchConfigs()).thenReturn(ImmutableSet.of(conf)); + doReturn(RunState.INACTIVE).when(element).getRunState(); + + element.openConfig(ui); + + verify(ui).openLaunchConfigurationDialogOnGroup(conf, IDebugUIConstants.ID_DEBUG_LAUNCH_GROUP); + verifyNoMoreInteractions(ui); + } + + @SuppressWarnings("unchecked") + @Test + public void openConfigWithTwoExistingConfs() throws Exception { + String projectName = "fooProject"; + IProject project = mockProject(projectName, true); + IJavaProject javaProject = mockJavaProject(project); + LocalRunTarget runTarget = mock(LocalRunTarget.class); + TestElement element = createElement(projectName, project, javaProject, runTarget); + UserInteractions ui = mock(UserInteractions.class); + ILaunchConfiguration conf1 = mock(ILaunchConfiguration.class); + ILaunchConfiguration conf2 = mock(ILaunchConfiguration.class); + + when(element.getLaunchConfigs()).thenReturn(ImmutableSet.of(conf1, conf2)); + doReturn(RunState.INACTIVE).when(element).getRunState(); + when(ui.chooseConfigurationDialog(anyString(), anyString(), listThat(hasItems(conf1, conf2)))) + .thenReturn(conf2); + + element.openConfig(ui); + + verify(ui).chooseConfigurationDialog( + stringContains("Choose", "Configuration"), + stringContains("Several"), + (List) any() + ); + verify(ui).openLaunchConfigurationDialogOnGroup(conf2, IDebugUIConstants.ID_DEBUG_LAUNCH_GROUP); + verifyNoMoreInteractions(ui); + } + + private List listThat(Matcher> iterMatcher) { + Object untyped = iterMatcher; + @SuppressWarnings("unchecked") + Matcher> listMatcher = (Matcher>) untyped; + return MockitoHamcrest.argThat(listMatcher); + } + + public static String stringContains(String... strings) { + return MockitoHamcrest.argThat(stringContainsInOrder(Arrays.asList(strings))); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/AllBootDashTests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/AllBootDashTests.java new file mode 100644 index 000000000..6ac70de8c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/AllBootDashTests.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * 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.test; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.ide.eclipse.boot.dash.docker.ui.SelectDockerDaemonDialog; +import org.springframework.ide.eclipse.boot.dash.test.actuator.ActuatorDataTest; +import org.springframework.ide.eclipse.boot.dash.test.yaml.AppNameReconcilerTest; +import org.springframework.ide.eclipse.boot.dash.test.yaml.CFRouteTests; +import org.springframework.ide.eclipse.boot.dash.test.yaml.DeploymentProperties2YamlTest; +import org.springframework.ide.eclipse.boot.dash.test.yaml.ManifestCompareMergeTests; +import org.springframework.ide.eclipse.boot.dash.test.yaml.Yaml2DeploymentPropertiesTest; + +@RunWith(Suite.class) +@SuiteClasses({ + //Tests suites are put in order roughly based on + // how long it takes to run them. Faster ones at the top. + + + // Raised up temporarily to be run first. + BootDashDockerTests.class, //50 seconds + SelectDockerDaemonDialogTest.class, + + //New: (move down the chain later based on runtime) + JmxSupportTest.class, + PropertyFileStoreTest.class, + + // Manifest YAML/Deployment Properties tests (less than 2 seconds per suite) + DeploymentProperties2YamlTest.class, + Yaml2DeploymentPropertiesTest.class, + AppNameReconcilerTest.class, + RouteBuilderTest.class, + CFRouteTests.class, + + //Really short (less than 2 seconds per suite): + JLRMethodParserTest.class, + OrderBasedComparatorTest.class, + ManifestCompareMergeTests.class, + AbstractLaunchConfigurationsDashElementTest.class, + BootDashElementTagsTests.class, + ActuatorDataTest.class, + ToggleFiltersModelTest.class, + + //Medium length (less than 30 seconds): + JarNameGeneratorTest.class, + BootDashViewModelTest.class, + BootJarPackagingTest.class, + BeanResourceDefinitionParsingTests.class, + + //Long tests (more than 30 seconds): + CloudFoundryBootDashModelMockingTest.class, + DeploymentPropertiesDialogModelTests.class, + BootDashActionTests.class, + BootDashModelTest.class, + CloudFoundryClientTest.class, + CloudFoundryBootDashModelIntegrationTest.class, +}) +public class AllBootDashTests { + + public static final String PLUGIN_ID = "org.springframework.ide.eclipse.boot.dash.test"; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/AllUserInteractions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/AllUserInteractions.java new file mode 100644 index 000000000..9654b6e4d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/AllUserInteractions.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2020 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.test; + +import org.springframework.ide.eclipse.boot.dash.cf.ui.CfUserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.docker.ui.DockerUserInteractions; + +public interface AllUserInteractions extends UserInteractions, CfUserInteractions, DockerUserInteractions { +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BeanResourceDefinitionParsingTests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BeanResourceDefinitionParsingTests.java new file mode 100644 index 000000000..a8bcedfa3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BeanResourceDefinitionParsingTests.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * Copyright (c) 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.test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.bootVersionAtLeast; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.setPackage; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.withStarters; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.createFile; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.debug.core.DebugPlugin; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.ide.eclipse.beans.ui.live.model.SpringResource; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +public class BeanResourceDefinitionParsingTests { + + private BootProjectTestHarness projects; + private TestBootDashModelContext context; + + @Before + public void setup() throws Exception { + StsTestUtil.deleteAllProjects(); + this.context = new TestBootDashModelContext(ResourcesPlugin.getWorkspace(), + DebugPlugin.getDefault().getLaunchManager()); + this.projects = new BootProjectTestHarness(context.getWorkspace()); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void classPathResource() throws Exception { + String resourceDefinition = "class path resource [org/springframework/boot/actuate/autoconfigure/audit/AuditAutoConfiguration.class]"; + String expectedPath = "org/springframework/boot/actuate/autoconfigure/audit/AuditAutoConfiguration.class"; + String expectedFQType = "org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration"; + SpringResource parser = new SpringResource(resourceDefinition, null); + String actualPath = parser.getResourcePath(); + String actualClassName = parser.getClassName(); + assertEquals(expectedPath, actualPath); + assertEquals(expectedFQType, actualClassName); + } + + @Test + public void classPathResourceWithProject() throws Exception { + // Tests parsing of type from a "class path resource" bean definition that is NOT in the project source, with a given project + // The purpose of this test is to ensure that passing a project to the parser does not affect + // parsing of type from "class path resource" + IProject project = createBootProject("test-bean-resource-file", + "src/main/java/com/example/demo/HelloController.java", "package com.example.demo;\n" + // + "\n" + // + "import org.springframework.web.bind.annotation.RequestMapping;\n" + // + "import org.springframework.web.bind.annotation.RestController;\n" + // + "\n" + // + "@RestController\n" + // + "public class HelloController {\n" + // + "\n" + // + " @RequestMapping(\"/hello\")\n" + // + " public String hello() {\n" + // + " return \"Hello, World!\";\n" + // + " }\n" + // + "\n" + // + "}\n"); + + // A resource definition that is NOT in the project source. + String resourceDefinition = "class path resource [org/springframework/boot/actuate/autoconfigure/audit/AuditAutoConfiguration.class]"; + String expectedPath = "org/springframework/boot/actuate/autoconfigure/audit/AuditAutoConfiguration.class"; + String expectedFQType = "org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration"; + + SpringResource parser = new SpringResource(resourceDefinition, project); + + String actualPath = parser.getResourcePath(); + String actualClassName = parser.getClassName(); + assertEquals(expectedPath, actualPath); + assertEquals(expectedFQType, actualClassName); + } + + @Test + public void classPathResourceInnerClass() throws Exception { + String resourceDefinition = "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration$MeterBindersConfiguration.class]"; + String expectedPath = "org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration$MeterBindersConfiguration.class"; + // Expected should be in JDT form using '.' to allow Inner Class navigation + String expectedFQType = "org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration.MeterBindersConfiguration"; + SpringResource parser = new SpringResource(resourceDefinition, null); + String actualPath = parser.getResourcePath(); + String actualClassName = parser.getClassName(); + assertEquals(expectedPath, actualPath); + assertEquals(expectedFQType, actualClassName); + } + + @Test + public void resourceDefinitionTypeOnly() throws Exception { + String resourceDefinition = "com/example/demo/HelloController.class"; + String expectedPath = "com/example/demo/HelloController.class"; + String expectedFQType = "com.example.demo.HelloController"; + SpringResource parser = new SpringResource(resourceDefinition, null); + String actualPath = parser.getResourcePath(); + String actualClassName = parser.getClassName(); + assertEquals(expectedPath, actualPath); + assertEquals(expectedFQType, actualClassName); + } + + @Test + public void resourceDefinitionTypeOnlyInnerClass() throws Exception { + String resourceDefinition = "com/example/demo/HelloController$InnerClass.class"; + String expectedPath = "com/example/demo/HelloController$InnerClass.class"; + String expectedFQType = "com.example.demo.HelloController.InnerClass"; + SpringResource parser = new SpringResource(resourceDefinition, null); + String actualPath = parser.getResourcePath(); + String actualClassName = parser.getClassName(); + assertEquals(expectedPath, actualPath); + assertEquals(expectedFQType, actualClassName); + } + + @Test + public void beanDefinitionIn() throws Exception { + String resourceDefinition = "BeanDefinition defined in org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration"; + String expectedPath = "org/springframework/security/oauth2/config/annotation/web/configuration/OAuth2ClientConfiguration.class"; + String expectedFQType = "org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration"; + SpringResource parser = new SpringResource(resourceDefinition, null); + String actualPath = parser.getResourcePath(); + String actualClassName = parser.getClassName(); + assertEquals(expectedPath, actualPath); + assertEquals(expectedFQType, actualClassName); + } + + @Test + public void fileDefinition1() throws Exception { + // To test this, we need to create a real project with an actual class file + IProject project = createBootProject("test-bean-resource-file", + "src/main/java/com/example/demo/HelloController.java", "package com.example.demo;\n" + // + "\n" + // + "import org.springframework.web.bind.annotation.RequestMapping;\n" + // + "import org.springframework.web.bind.annotation.RestController;\n" + // + "\n" + // + "@RestController\n" + // + "public class HelloController {\n" + // + "\n" + // + " @RequestMapping(\"/hello\")\n" + // + " public String hello() {\n" + // + " return \"Hello, World!\";\n" + // + " }\n" + // + "\n" + // + "}\n"); + + // To test a resource definition with a path to a bean type in a project, we need the absolute path + // of that type file, as that is what we'd get from "real" actuator info. + // However, we can't hardcode the absolute project path that would contain that type ahead of time as this test is run on different environments, + // so we have to construct a resource definition to test with the project path as shown below. + String resourceDefinition = "file [" + project.getLocation() + "/target/classes/com/example/demo/HelloController.class]"; + + String expectedFQType = "com.example.demo.HelloController"; + SpringResource parser = new SpringResource(resourceDefinition, project); + String actualClassName = parser.getClassName(); + assertEquals(expectedFQType, actualClassName); + } + + @Test + public void fileDefinitionInnerClassBean() throws Exception { + // To test this, we need to create a real project with an actual class file + IProject project = createBootProject("test-bean-resource-file", + "src/main/java/com/example/demo/HelloController.java", "package com.example.demo;\n" + // + "\n" + // + "import org.springframework.web.bind.annotation.RequestMapping;\n" + // + "import org.springframework.web.bind.annotation.RestController;\n" + // + "\n" + // + "@RestController\n" + // + "public class HelloController {\n" + // + "\n" + // + " @RequestMapping(\"/hello\")\n" + // + " public String hello() {\n" + // + " return \"Hello, World!\";\n" + // + " }\n" + // + " @Component\n" + // + " static public class InnerClass {\n" + // + "\n" + // + " /**\n" + // + " * \n" + // + " */\n" + // + " public InnerClass() {\n" + // + " super();\n" + // + " }\n" + // + "\n" + // + " }\n" + // + "\n" + // + " @Autowired private InnerClass innerclass;" + // + "\n" + // + "}\n");// + + // To test a resource definition with a path to a bean type in a project, we need the absolute path + // of that type file, as that is what we'd get from "real" actuator info. + // However, we can't hardcode the absolute project path that would contain that type ahead of time as this test is run on different environments, + // so we have to construct a resource definition to test with the project path as shown below. + String resourceDefinition = project.getLocation() + "/target/classes/com/example/demo/HelloController$InnerClass.class"; + + String expectedFQType = "com.example.demo.HelloController.InnerClass"; + SpringResource parser = new SpringResource(resourceDefinition, project); + String actualClassName = parser.getClassName(); + assertEquals(expectedFQType, actualClassName); + } + + private IProject createBootProject(String projectName, String typePath, String clsContent) throws Exception { + + IProject project = projects.createBootWebProject(projectName, bootVersionAtLeast("1.3.0"), //required for us to be able to determine the actuator port + withStarters("web", "actuator"), //required to actually *have* an actuator + setPackage("com.example.demo")); + + createFile(project, typePath, clsContent); + return project; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashActionTests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashActionTests.java new file mode 100644 index 000000000..bf30a71da --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashActionTests.java @@ -0,0 +1,1031 @@ +/******************************************************************************* + * 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jface.action.IAction; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTargetType; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveDataConnectionManagementActions; +import org.springframework.ide.eclipse.boot.dash.model.AbstractLaunchConfigurationsDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement; +import org.springframework.ide.eclipse.boot.dash.model.LocalBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.LocalRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.RunTargets; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockLiveProcesses; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockLiveProcesses.MockProcess; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockMultiSelection; +import org.springframework.ide.eclipse.boot.dash.views.BootDashActions; +import org.springframework.ide.eclipse.boot.dash.views.BootDashActions.RunOrDebugStateAction; +import org.springframework.ide.eclipse.boot.dash.views.DuplicateConfigAction; +import org.springframework.ide.eclipse.boot.dash.views.OpenLaunchConfigAction; +import org.springframework.ide.eclipse.boot.dash.views.RunStateAction; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.livebean.JmxBeanSupport; +import org.springframework.ide.eclipse.boot.test.AutobuildingEnablement; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.WizardConfigurer; +import org.springframework.ide.eclipse.boot.test.util.TestBracketter; +import org.springsource.ide.eclipse.commons.core.pstore.InMemoryPropertyStore; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +/** + * @author Kris De Volder + */ +public class BootDashActionTests { + + @Test + public void liveProcessManagementVisibilityForLocalElements() throws Exception { + String projectName = "santa-baby"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + AbstractLaunchConfigurationsDashElement projectElement = (AbstractLaunchConfigurationsDashElement) harness.getElementWithName(projectName); + BootDashElement launchConfElement = harness.getElementFor(BootLaunchConfigurationDelegate.createConf(javaProject)); + LiveDataConnectionManagementActions liveActionsMenu = actions.getLiveDataConnectionManagement(); + + MockMultiSelection selection = harness.selection; + + assertTrue(selection.getElements().isEmpty()); + assertTrue(liveActionsMenu.isVisible()); + + selection.setElements(projectElement); + assertTrue(liveActionsMenu.isVisible()); + + selection.setElements(launchConfElement); + assertFalse(liveActionsMenu.isVisible()); + } + + @Test + public void liveProcessManagementEnablementForSingleLocalElement() throws Exception { + LiveVariable runState = new LiveVariable<>(RunState.INACTIVE); +// runState.onChange((e, s) -> { +// harness.model.notify(); +// }); + LiveDataConnectionManagementActions liveActionsMenu = actions.getLiveDataConnectionManagement(); + + String projectName = "santa-baby"; + IProject project = mock(IProject.class); + when(project.getName()).thenReturn(projectName); + + RunTarget runTarget = LocalRunTarget.INSTANCE; + + LocalBootDashModel bdm = mock(LocalBootDashModel.class); + when(bdm.getRunTarget()).thenReturn(runTarget); + + //mocked BootProjectDashElement where we can easily control the runstate from the test case: + BootProjectDashElement element = mockRunnableProjectElement(project, runState); + + harness.selection.setElements(element); + + + for (RunState state : RunState.values()) { + System.out.println("RunState = "+state); + runState.setValue(state); + Boolean enabled = liveActionsMenu.isEnabled().getValue(); + System.out.println("Enabled = "+enabled); + assertEquals(state, element.getRunState()); + assertEquals(state==RunState.RUNNING || state==RunState.DEBUGGING, enabled); + } + } + + private BootProjectDashElement mockRunnableProjectElement(IProject project, LiveVariable runState) { + return new BootProjectDashElement(project, harness.getLocalTargetModel(), context.getProjectProperties(), null, null) { + //This is what we care about really: + protected LiveExpression createRunStateExp() { + return runState; + } + }; + } + + @Test + public void liveProcessManagementEnablementForMultipleLocalElement() throws Exception { + //setup: + String projectName = "santa"; + IProject project = mock(IProject.class); + when(project.getName()).thenReturn(projectName); + + String otherProjectName = "baby"; + IProject otherProject = mock(IProject.class); + when(otherProject.getName()).thenReturn(otherProjectName); + + LiveVariable elementState = new LiveVariable<>(RunState.INACTIVE); + BootProjectDashElement element = mockRunnableProjectElement(project, elementState); + + LiveVariable otherElementState = new LiveVariable<>(RunState.INACTIVE); + BootProjectDashElement otherElement = mockRunnableProjectElement(otherProject, otherElementState); + + LiveDataConnectionManagementActions liveActionsMenu = actions.getLiveDataConnectionManagement(); + + //test: two elements: + harness.selection.setElements(element, otherElement); + assertTrue(liveActionsMenu.isVisible()); + for (RunState s1 : RunState.values()) { + for (RunState s2 : RunState.values()) { + boolean expectEnabled = s1==RunState.RUNNING || s2==RunState.RUNNING || s1==RunState.DEBUGGING || s2==RunState.DEBUGGING; + elementState.setValue(s1); + otherElementState.setValue(s2); + assertEquals(expectEnabled, liveActionsMenu.isEnabled().getValue()); + } + } + + //test: no elements + elementState.setValue(RunState.RUNNING); + otherElementState.setValue(RunState.DEBUGGING); + harness.selection.setElements(/*none*/); + assertTrue(liveActionsMenu.isVisible()); + assertEquals(true, liveActionsMenu.isEnabled().getValue()); + } + + @Test + public void liveProcessManagementSingleProjectSelection() throws Exception { + //setup: + String[] projectNames = { "a", "b", "c" }; + int[] processCounts = {1, 2, 0}; + List> mockProcesses = new ArrayList<>(); + + for (int i = 0; i < projectNames.length; i++) { + String projectName = projectNames[i]; + IProject project = createBootProject(projectName); + ArrayList list = new ArrayList<>(); + mockProcesses.add(list); + for (int p = 0; p < processCounts[i]; p++) { + list.add(processes.newLocalProcess(projectName+":"+p, project, p)); + } + } + + LiveDataConnectionManagementActions liveActionsMenu = actions.getLiveDataConnectionManagement(); + MockMultiSelection selection = harness.selection; + + //test: + for (int i = 0; i < projectNames.length; i++) { + BootDashElement projectElement = harness.getElementWithName(projectNames[i]); + selection.setElements(projectElement); + int numprocs = processCounts[i]; + if (numprocs==0) { + List actions = liveActionsMenu.getActions(); + assertEquals(1, actions.size()); + IAction action = actions.get(0); + assertEquals("No matching processes", action.getText()); + assertFalse(action.isEnabled()); + } else { + Stream processNames = mockProcesses.get(i).stream().map(MockProcess::getProcessKey); + List actions = liveActionsMenu.getActions(); + for (IAction a : actions) { + assertTrue(a.isEnabled()); + } + assertEquals(processCounts[i], actions.size()); + + Set expectedLabels = processNames.map(name -> "Connect "+name+" lbl").collect(Collectors.toSet()); + Set actualLabels = actions.stream().map(a -> a.getText()).collect(Collectors.toSet()); + assertEquals(expectedLabels, actualLabels); + } + } + } + + @Test + public void liveProcessManagementMultipleOrEmptyProjectSelection() throws Exception { + //setup: + String[] projectNames = { "a", "b", "c" }; + int[] processCounts = {1, 2, 1}; + List> mockProcesses = new ArrayList<>(); + + for (int i = 0; i < projectNames.length; i++) { + String projectName = projectNames[i]; + IProject project = createBootProject(projectName); + ArrayList list = new ArrayList<>(); + mockProcesses.add(list); + for (int p = 0; p < processCounts[i]; p++) { + list.add(processes.newLocalProcess(projectName+":"+p, project, p)); + } + } + + LiveDataConnectionManagementActions liveActionsMenu = actions.getLiveDataConnectionManagement(); + MockMultiSelection selection = harness.selection; + + BootDashElement a = harness.getElementWithName("a"); + BootDashElement b = harness.getElementWithName("b"); + BootDashElement c = harness.getElementWithName("c"); + + //test a and b selected + { + selection.setElements(a, b); + List actions = liveActionsMenu.getActions(); + Set actualLabels = actions.stream().map(IAction::getText).collect(Collectors.toSet()); + assertEquals(ImmutableSet.of("Connect a:0 lbl", "Connect b:0 lbl", "Connect b:1 lbl"), actualLabels); + for (IAction ac : actions) { + assertTrue(ac.isEnabled()); + } + } + + //test b and c selected + { + selection.setElements(b, c); + List actions = liveActionsMenu.getActions(); + Set actualLabels = actions.stream().map(IAction::getText).collect(Collectors.toSet()); + assertEquals(ImmutableSet.of("Connect c:0 lbl", "Connect b:0 lbl", "Connect b:1 lbl"), actualLabels); + for (IAction ac : actions) { + assertTrue(ac.isEnabled()); + } + } + + //no elements selected + { + selection.setElements(/*none*/); + List actions = liveActionsMenu.getActions(); + Set actualLabels = actions.stream().map(IAction::getText).collect(Collectors.toSet()); + assertEquals(ImmutableSet.of("Connect a:0 lbl", "Connect c:0 lbl", "Connect b:0 lbl", "Connect b:1 lbl"), actualLabels); + for (IAction ac : actions) { + assertTrue(ac.isEnabled()); + } + } + } + + @Test + public void liveProcessManagementProcessWithoutProjectName() throws Exception { + IProject project = createBootProject("a"); + processes.newLocalProcess("whatever", null, 1); + processes.newLocalProcess("a-process", project, 2); + + + harness.selection.setElements(harness.getElementFor(project)); + Set labels = actions.getLiveDataConnectionManagement().getActions().stream().map(a -> a.getText()).collect(Collectors.toSet()); + assertEquals(ImmutableSet.of("Connect a-process lbl"), labels); + } + + @Test + public void liveProcessManagementScenario() throws Exception { + IProject project = createBootProject("a"); + MockProcess process = processes.newLocalProcess("a-process", project, 1); + + LiveDataConnectionManagementActions actionProvider = actions.getLiveDataConnectionManagement(); + harness.selection.setElements(harness.getElementFor(project)); + + assertFalse(process.isConnected()); + process.assertRefreshed(0); + + IAction action = assertSingleEnabledActionWithLabel(actionProvider.getActions(), "Connect a-process lbl"); + action.run(); + assertTrue(process.isConnected()); + process.assertRefreshed(0); + + { + List actions = actionProvider.getActions(); + assertEquals(2, actions.size()); + IAction refreshAction = assertActionWithLabel(actions, "Refresh a-process lbl"); + IAction disconnectAction = assertActionWithLabel(actions, "Disconnect a-process lbl"); + refreshAction.run(); + + assertTrue(process.isConnected()); + process.assertRefreshed(1); + } + + { + List actions = actionProvider.getActions(); + assertEquals(2, actions.size()); + IAction refreshAction = assertActionWithLabel(actions, "Refresh a-process lbl"); + IAction disconnectAction = assertActionWithLabel(actions, "Disconnect a-process lbl"); + disconnectAction.run(); + + assertFalse(process.isConnected()); + } + + assertSingleEnabledActionWithLabel(actionProvider.getActions(), "Connect a-process lbl"); + } + + @Test + public void liveProcessManagementMixedLocalAndRemoteProcesses() throws Exception { + BootProjectDashElement localEl = harness.getElementFor(projects.createBootProject("webaaa")); + + String appGuid = "7957d372-d68b-4233-aebc-41f94cdab318"; + MockProcess remoteApp = processes.newProcess(ImmutableMap.of( + "processKey", "remote process 1", + "label", "remote process 1 (lbl)", + "processId", appGuid + )); + + MockProcess localApp = processes.newProcess(ImmutableMap.of( + "processKey", "4433 - com.example.demo.WebaaaApplication", + "label", "4433 (com.example.demo.WebaaaApplication)", + "projectName", "webaaa", + "processId", "4433" + )); + + LiveDataConnectionManagementActions liveActionsMenu = actions.getLiveDataConnectionManagement(); + + MockMultiSelection selection = harness.selection; + + //no elements selected + { + selection.setElements(/*none*/); + List actions = liveActionsMenu.getActions(); + Set actualLabels = actions.stream().map(IAction::getText).collect(Collectors.toSet()); + assertEquals(ImmutableSet.of("Connect remote process 1 (lbl)", "Connect 4433 (com.example.demo.WebaaaApplication)"), actualLabels); + for (IAction ac : actions) { + assertTrue(ac.isEnabled()); + } + } + + //local element selected + { + selection.setElements(localEl); + List actions = liveActionsMenu.getActions(); + Set actualLabels = actions.stream().map(IAction::getText).collect(Collectors.toSet()); + assertEquals(ImmutableSet.of("Connect 4433 (com.example.demo.WebaaaApplication)"), actualLabels); + for (IAction ac : actions) { + assertTrue(ac.isEnabled()); + } + } + + { + CloudAppDashElement cfElement = mockCfElement(appGuid, "demo-app"); + selection.setElements(cfElement); + List actions = liveActionsMenu.getActions(); + Set actualLabels = actions.stream().map(IAction::getText).collect(Collectors.toSet()); + assertEquals(ImmutableSet.of("Connect remote process 1 (lbl)"), actualLabels); + for (IAction ac : actions) { + assertTrue(ac.isEnabled()); + } + } + } + + private IAction assertActionWithLabel(List actions, String expectedLabel) { + Optional found = actions.stream().filter(a -> expectedLabel.equals(a.getText())).findFirst(); + assertTrue(found.isPresent()); + return found.get(); + } + + private IAction assertSingleEnabledActionWithLabel(List list, String expectedLabel) { + assertEquals(1, list.size()); + IAction action = list.get(0); + assertEquals(expectedLabel, action.getText()); + return action; + } + + @Test + public void deleteConfigActionEnablementForProject() throws Exception { + //At the moment, this action does not enable for projects at all + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + final AbstractLaunchConfigurationsDashElement element = (AbstractLaunchConfigurationsDashElement) harness.getElementWithName(projectName); + + MockMultiSelection selection = harness.selection; + BootLaunchConfigurationDelegate.createConf(javaProject); + + final IAction action = actions.getDeleteConfigsAction(); + action.setEnabled(true); //force it to true so we can tell that it actually changes. + selection.setElements(element); + new ACondition("Wait for disablement", 3000) { + public boolean test() throws Exception { + assertFalse(action.isEnabled()); + return true; + } + }; + } + + @Test + public void deleteConfigActionEnablementForConf() throws Exception { + //At the moment, this action always enables for one or more launch confs + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + final AbstractLaunchConfigurationsDashElement element = (AbstractLaunchConfigurationsDashElement) harness.getElementWithName(projectName); + + MockMultiSelection selection = harness.selection; + final ILaunchConfiguration conf1 = BootLaunchConfigurationDelegate.createConf(javaProject); + BootDashElement el1 = harness.getElementFor(conf1); + final ILaunchConfiguration conf2 = BootLaunchConfigurationDelegate.createConf(javaProject); + BootDashElement el2 = harness.getElementFor(conf2); + + new ACondition("Wait for children", 3000) { + public boolean test() throws Exception { + assertEquals(2, element.getCurrentChildren().size()); + return true; + } + }; + + final IAction action = actions.getDeleteConfigsAction(); + + assertFalse(action.isEnabled()); + selection.setElements(el1); + new ACondition("Wait for enablement", 3000) { + public boolean test() throws Exception { + assertTrue(action.isEnabled()); + return true; + } + }; + + action.setEnabled(false); + selection.setElements(el1, el2); + new ACondition("Wait for enablement", 3000) { + public boolean test() throws Exception { + assertTrue(action.isEnabled()); + return true; + } + }; + + } + + @Test + public void deleteConfigAction() throws Exception { + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + final AbstractLaunchConfigurationsDashElement element = (AbstractLaunchConfigurationsDashElement) harness.getElementWithName(projectName); + + MockMultiSelection selection = harness.selection; + final ILaunchConfiguration conf1 = BootLaunchConfigurationDelegate.createConf(javaProject); + final BootDashElement el1 = harness.getElementFor(conf1); + final ILaunchConfiguration conf2 = BootLaunchConfigurationDelegate.createConf(javaProject); + final BootDashElement el2 = harness.getElementFor(conf2); + + new ACondition("Wait for children", 3000) { + public boolean test() throws Exception { + assertEquals(2, element.getCurrentChildren().size()); + assertEquals(ImmutableSet.of(el1, el2), element.getCurrentChildren()); + return true; + } + }; + + final IAction action = actions.getDeleteConfigsAction(); + + assertFalse(action.isEnabled()); + selection.setElements(el1); + new ACondition("Wait for enablement", 3000) { + public boolean test() throws Exception { + assertTrue(action.isEnabled()); + return true; + } + }; + + when(ui().confirmOperation(eq("Deleting Elements"), anyString())).thenReturn(true); + action.run(); + + new ACondition("Wait for config deletion", 3000) { + public boolean test() throws Exception { + assertEquals(ImmutableSet.of(conf2), element.getLaunchConfigs()); + assertFalse(conf1.exists()); + assertTrue(element.getCurrentChildren().size()==1); + return true; + } + }; + } + + + private UserInteractions ui() { + return context.injections.getBean(AllUserInteractions.class); + } + + @Test + public void duplicateConfigAction() throws Exception { + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + final AbstractLaunchConfigurationsDashElement element = (AbstractLaunchConfigurationsDashElement) harness.getElementWithName(projectName); + + MockMultiSelection selection = harness.selection; + final DuplicateConfigAction action = actions.getDuplicateConfigAction(); + final ILaunchConfiguration conf1 = BootLaunchConfigurationDelegate.createConf(javaProject); + assertEquals(0, getJMXPortAsInt(conf1)); + selection.setElements(element); + new ACondition("Wait for enablement", 3000) { + public boolean test() throws Exception { + assertTrue(action.isEnabled()); + return true; + } + }; + + action.run(); + + new ACondition("Wait for action post conditions", 3000) { + public boolean test() throws Exception { + ImmutableSet confs = element.getLaunchConfigs(); + assertEquals(2, confs.size()); + assertEquals(2, element.getCurrentChildren().size()); + assertTrue(confs.contains(conf1)); + for (ILaunchConfiguration other : confs) { + assertEquals(0, getJMXPortAsInt(other)); + } + return true; + } + + }; + } + + @Test + public void duplicateConfigActionWithJmxPortSet() throws Exception { + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + final AbstractLaunchConfigurationsDashElement element = (AbstractLaunchConfigurationsDashElement) harness.getElementWithName(projectName); + + MockMultiSelection selection = harness.selection; + final DuplicateConfigAction action = actions.getDuplicateConfigAction(); + final ILaunchConfiguration conf1 = BootLaunchConfigurationDelegate.createConf(javaProject); + assertEquals(0, getJMXPortAsInt(conf1)); + String randomPort = ""+JmxBeanSupport.randomPort(); + setJMXPort(conf1, randomPort); + assertEquals(randomPort, BootLaunchConfigurationDelegate.getJMXPort(conf1)); + selection.setElements(element); + new ACondition("Wait for enablement", 3000) { + public boolean test() throws Exception { + assertTrue(action.isEnabled()); + return true; + } + }; + + action.run(); + + new ACondition("Wait for action post conditions", 3000) { + public boolean test() throws Exception { + ImmutableSet confs = element.getLaunchConfigs(); + assertEquals(2, confs.size()); + assertEquals(2, element.getCurrentChildren().size()); + assertTrue(confs.contains(conf1)); + for (ILaunchConfiguration other : confs) { + if (!other.equals(conf1)) { + assertFalse(getJMXPortAsInt(conf1)==getJMXPortAsInt(other)); + } + } + return true; + } + + }; + } + + private static void setJMXPort(ILaunchConfiguration conf, String port) throws CoreException { + ILaunchConfigurationWorkingCopy wc = conf.getWorkingCopy(); + BootLaunchConfigurationDelegate.setJMXPort(wc, port); + wc.doSave(); + } + + private static int getJMXPortAsInt(ILaunchConfiguration conf) { + try { + String str = BootLaunchConfigurationDelegate.getJMXPort(conf); + if (str!=null) { + return Integer.parseInt(str); + } + } catch (NumberFormatException e) { + //couldn't parse + } + return -1; + } + + @Test + public void duplicateConfigActionEnablementForLaunchConf() throws Exception { + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + final AbstractLaunchConfigurationsDashElement element = (AbstractLaunchConfigurationsDashElement) harness.getElementWithName(projectName); + + MockMultiSelection selection = harness.selection; + final DuplicateConfigAction action = actions.getDuplicateConfigAction(); + + ILaunchConfiguration conf1 = BootLaunchConfigurationDelegate.createConf(javaProject); + ILaunchConfiguration conf2 = BootLaunchConfigurationDelegate.createConf(javaProject); + new ACondition("Wait for elements", 3000) { + public boolean test() throws Exception { + assertEquals(2, element.getLaunchConfigs().size()); + assertEquals(2, element.getCurrentChildren().size()); + return true; + } + }; + + BootDashElement el1 = harness.getElementFor(conf1); + BootDashElement el2 = harness.getElementFor(conf2); + + assertFalse(action.isEnabled()); // or test may pass vacuously without an actual update + selection.setElements(el1); + new ACondition("Wait for enablement", 3000) { + public boolean test() throws Exception { + assertTrue(action.isEnabled()); + return true; + } + }; + + selection.setElements(el1, el2); + new ACondition("Wait for disablement", 3000) { + public boolean test() throws Exception { + assertFalse(action.isEnabled()); + return true; + } + }; + } + + @Test + public void duplicateConfigActionEnablementForProject() throws Exception { + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + final AbstractLaunchConfigurationsDashElement element = (AbstractLaunchConfigurationsDashElement) harness.getElementWithName(projectName); + + MockMultiSelection selection = harness.selection; + final DuplicateConfigAction action = actions.getDuplicateConfigAction(); + + //If selection is empty the action must not be enabled + assertTrue(selection.isEmpty()); + assertFalse(action.isEnabled()); + + //If project is selected then... + selection.setElements(element); + // a) if project has no launch configs ... + assertTrue(element.getLaunchConfigs().isEmpty()); + // then there's nothing to duplicate... so disabled + assertFalse(action.isEnabled()); + + // b) if project has exactly one launch config ... + BootLaunchConfigurationDelegate.createConf(javaProject); + // action enablement is updated as response to some asynchronous state changes + // so may not happen immediately + new ACondition("Wait for enablement", 3000) { + public boolean test() throws Exception { + assertEquals(1, element.getLaunchConfigs().size()); + assertTrue(action.isEnabled()); + return true; + } + }; + + // c) if project has more than one launch config... + BootLaunchConfigurationDelegate.createConf(javaProject); + ACondition.waitFor("Launch conf elements", 3000, () -> { + assertEquals(2, element.getLaunchConfigs().size()); + }); + // ... async update may not happen right away... + new ACondition("Wait for disablement", 3000) { + public boolean test() throws Exception { + assertEquals(2, element.getCurrentChildren().size()); + assertFalse(action.isEnabled()); + return true; + } + }; + + } + + @Test + public void openConfigActionEnablementForProject() throws Exception { + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + final BootDashElement element = harness.getElementWithName(projectName); + + MockMultiSelection selection = harness.selection; + final OpenLaunchConfigAction action = actions.getOpenConfigAction(); + + //If selection is empty the action must not be enabled + assertTrue(selection.isEmpty()); + assertFalse(action.isEnabled()); + + //If selection has more than one element... the action must not be enabled + selection.setElements(element, mockLocalElement()); + assertFalse(action.isEnabled()); + + //If selection has one element... + selection.setElements(element); + + //a) and element has no launch configs... + assertTrue(element.getLaunchConfigs().isEmpty()); + assertTrue(action.isEnabled()); + + // Careful... when changing the launch configs of a element, the enablement state of + // action should auto-refresh, but this happens asyncly so the tests sequences are put in such + // a order that the enablement state changes on each (otherwise the ACondition may vacuously + // pass immediately even if the enablement didn't get updated, as it was correct from + // the start) + + //b) and element has multiple launch config + assertTrue(action.isEnabled()); // make sure the test won't pass 'by accident'. + final ILaunchConfiguration c1 = BootLaunchConfigurationDelegate.createConf(javaProject); + final ILaunchConfiguration c2 = BootLaunchConfigurationDelegate.createConf(javaProject); + new ACondition(2000) { + public boolean test() throws Exception { + assertEquals(ImmutableSet.of(c1,c2), element.getLaunchConfigs()); + assertFalse(action.isEnabled()); + return true; + } + }; + + //b) and element has a single launch config + assertFalse(action.isEnabled()); // make sure the test won't pass 'by accident'. + c2.delete(); + new ACondition(2000) { + public boolean test() throws Exception { + assertEquals(ImmutableSet.of(c1), element.getLaunchConfigs()); + assertTrue(action.isEnabled()); + return true; + } + }; + } + + @Test + public void openConfigActionEnablementForLaunchConfig() throws Exception { + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + final BootDashElement element = harness.getElementWithName(projectName); + + MockMultiSelection selection = harness.selection; + final OpenLaunchConfigAction action = actions.getOpenConfigAction(); + + BootLaunchConfigurationDelegate.createConf(javaProject); + BootLaunchConfigurationDelegate.createConf(javaProject); + + //Check initial conditions are as expected: + assertTrue(selection.isEmpty()); + assertFalse(action.isEnabled()); + ACondition.waitFor("children to appear", 3000, () -> { + assertEquals(2, element.getChildren().getValues().size()); + }); + + //Check action enablement for the children + for (BootDashElement child : element.getChildren().getValues()) { + selection.setElements(child); + assertTrue(action.isEnabled()); + } + } + + @Test + public void openRedebugActionEnablementForLaunchConfig() throws Exception { + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + final BootDashElement element = harness.getElementWithName(projectName); + + MockMultiSelection selection = harness.selection; + final RunStateAction action = getRunStateAction(RunState.DEBUGGING); + + BootLaunchConfigurationDelegate.createConf(javaProject); + BootLaunchConfigurationDelegate.createConf(javaProject); + + //Check initial conditions are as expected: + assertTrue(selection.isEmpty()); + assertFalse(action.isEnabled()); + ACondition.waitFor("children", 3000, () -> { + assertEquals(2, element.getChildren().getValues().size()); + }); + + //Check action enablement for the children + for (BootDashElement child : element.getChildren().getValues()) { + selection.setElements(child); + assertTrue(action.isEnabled()); + } + } + + @Test + public void redebugActionEnablementForMultipleProjects() throws Exception { + IProject p1 = createBootProject("project1"); + IProject p2 = createBootProject("project2"); + + BootDashElement e1 = harness.getElementWithName(p1.getName()); + BootDashElement e2 = harness.getElementWithName(p2.getName()); + + MockMultiSelection selection = harness.selection; + final RunStateAction action = getRunStateAction(RunState.DEBUGGING); + + assertTrue(selection.isEmpty()); + assertFalse(action.isEnabled()); + + selection.setElements(e1, e2); + + assertTrue(action.isEnabled()); + } + + @Test + public void restartActionEnablementForProject() throws Exception { + doRestartActionEnablementForProjectTest(RunState.RUNNING); + } + + @Test + public void redebugActionEnablementForProject() throws Exception { + doRestartActionEnablementForProjectTest(RunState.DEBUGGING); + } + + private void doRestartActionEnablementForProjectTest(RunState runOrDebug) throws Exception, CoreException { + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + final BootDashElement element = harness.getElementWithName(projectName); + + MockMultiSelection selection = harness.selection; + final RunStateAction action = getRunStateAction(runOrDebug); + + //If selection is empty the action must not be enabled + assertTrue(selection.isEmpty()); + assertFalse(action.isEnabled()); + + //If selection has one element... + selection.setElements(element); + + //a) and element has no launch configs... + assertTrue(element.getLaunchConfigs().isEmpty()); + assertTrue(action.isEnabled()); + + //b) and element has multiple launch config + action.setEnabled(false); // make sure the test won't pass 'by accident'. + final ILaunchConfiguration c1 = BootLaunchConfigurationDelegate.createConf(javaProject); + final ILaunchConfiguration c2 = BootLaunchConfigurationDelegate.createConf(javaProject); + new ACondition(2000) { + public boolean test() throws Exception { + assertEquals(ImmutableSet.of(c1,c2), element.getLaunchConfigs()); + assertTrue(action.isEnabled()); + return true; + } + }; + + //b) and element has a single launch config + action.setEnabled(false); // make sure the test won't pass 'by accident'. + c2.delete(); + new ACondition(2000) { + public boolean test() throws Exception { + assertEquals(ImmutableSet.of(c1), element.getLaunchConfigs()); + assertTrue(action.isEnabled()); + return true; + } + }; + } + + @Test + public void restartActionEnablementForMultipleProjects() throws Exception { + IProject p1 = createBootProject("project1"); + IProject p2 = createBootProject("project2"); + + BootDashElement e1 = harness.getElementWithName(p1.getName()); + BootDashElement e2 = harness.getElementWithName(p2.getName()); + + MockMultiSelection selection = harness.selection; + final RunStateAction action = getRunStateAction(RunState.RUNNING); + + assertTrue(selection.isEmpty()); + assertFalse(action.isEnabled()); + + selection.setElements(e1, e2); + + assertTrue(action.isEnabled()); + } + + @Test + public void restartActionTargetsChildrenDirectly() throws Exception { + String projectName = "hohoho"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + + MockMultiSelection selection = harness.selection; + final BootDashElement element = harness.getElementWithName(projectName); + final ILaunchConfiguration c1 = BootLaunchConfigurationDelegate.createConf(javaProject); + final ILaunchConfiguration c2 = BootLaunchConfigurationDelegate.createConf(javaProject); + BootDashElement child1 = harness.getElementFor(c1); + BootDashElement child2 = harness.getElementFor(c2); + + ImmutableSet theChildren = ImmutableSet.of( + child1, child2 + ); + ACondition.waitFor("children to appear", 3000, () -> { + assertEquals(theChildren, element.getChildren().getValues()); + }); + + for (RunState runOrDebug: EnumSet.of(RunState.RUNNING, RunState.DEBUGGING)) { + final RunOrDebugStateAction action = (RunOrDebugStateAction) getRunStateAction(runOrDebug); + selection.setElements(/*none*/); + assertEquals(ImmutableSet.of(), action.getSelectedElements()); + assertEquals(ImmutableSet.of(), action.getTargetElements()); + + selection.setElements(element); + assertEquals(ImmutableSet.of(element), action.getSelectedElements()); + assertEquals(""+runOrDebug, theChildren, action.getTargetElements()); + + selection.setElements(element, child1); + assertEquals(ImmutableSet.of(element, child1), action.getSelectedElements()); + assertEquals(theChildren, action.getTargetElements()); + } + } + + //////////////////////////////////////////////////////////////////////// + + + private TestBootDashModelContext context; + private BootProjectTestHarness projects; + private BootDashViewModelHarness harness; + private BootDashActions actions; + + @Rule + public AutobuildingEnablement autobuild = new AutobuildingEnablement(false); + + @Rule + public TestBracketter bracketter = new TestBracketter(); + + @Rule + public LaunchCleanups launchCleanups = new LaunchCleanups(); + + private MockLiveProcesses processes; + + @Before + public void setup() throws Exception { + StsTestUtil.deleteAllProjects(); + this.context = new TestBootDashModelContext( + ResourcesPlugin.getWorkspace(), + DebugPlugin.getDefault().getLaunchManager() + ); + SimpleDIContext injections = context.injections; + + this.processes = new MockLiveProcesses(); + this.harness = new BootDashViewModelHarness(context.withTargetTypes(RunTargetTypes.LOCAL)); + this.projects = new BootProjectTestHarness(context.getWorkspace()); + this.actions = new BootDashActions(harness.model, harness.selection.forReading(), injections, processes.commandExecutor); + } + + @After + public void tearDown() throws Exception { + this.harness.dispose(); + this.actions.dispose(); + } + + + private IProject createBootProject(String projectName, WizardConfigurer... extraConfs) throws Exception { + return projects.createBootWebProject(projectName, extraConfs); + } + + private RunStateAction getRunStateAction(RunState goalState) { + for (RunStateAction s : actions.getRunStateActions()) { + if (s.getGoalState()==goalState) { + return s; + } + } + return null; + } + + private BootDashElement mockLocalElement() { + BootDashElement element = mock(BootDashElement.class); + RunTarget target = RunTargets.LOCAL; + when(element.getBootDashModel()).thenReturn(harness.getRunTargetModel(RunTargetTypes.LOCAL)); + when(element.getTarget()).thenReturn(target); + when(element.supportedGoalStates()).thenReturn(RunTargets.LOCAL_RUN_GOAL_STATES); + return element; + } + + private CloudAppDashElement mockCfElement(String appGuid, String appName) { + CloudFoundryRunTarget cfTarget = mock(CloudFoundryRunTarget.class); + CloudFoundryRunTargetType cfType = mock(CloudFoundryRunTargetType.class); + when(cfTarget.getType()).thenReturn(cfType); + + CloudFoundryBootDashModel model = mock(CloudFoundryBootDashModel.class); + when(model.getRunTarget()).thenReturn(cfTarget); + CloudAppDashElement element = spy(new CloudAppDashElement(model, appName, new InMemoryPropertyStore())); + when(element.getAppGuid()).thenReturn(UUID.fromString(appGuid)); + when(element.getName()).thenReturn(appName); + when(element.supportedGoalStates()).thenReturn(CloudFoundryRunTarget.RUN_GOAL_STATES); + when(element.getTarget()).thenReturn(cfTarget); + return element; + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashDockerTests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashDockerTests.java new file mode 100644 index 000000000..ebc6bcb07 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashDockerTests.java @@ -0,0 +1,1519 @@ +/******************************************************************************* + * Copyright (c) 2020 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.bootVersionAtLeast; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.withImportStrategy; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.withStarters; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.assertContains; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.assertNotContains; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.createFile; + +import java.net.URI; +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +import org.apache.commons.io.IOUtils; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jface.action.IAction; +import org.eclipse.swt.graphics.Color; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansModel; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.RemoteBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeployToRemoteTargetAction; +import org.springframework.ide.eclipse.boot.dash.console.ApplicationLogConsole; +import org.springframework.ide.eclipse.boot.dash.console.CloudAppLogManager; +import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsUtil; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.docker.runtarget.DockerApp; +import org.springframework.ide.eclipse.boot.dash.docker.runtarget.DockerContainer; +import org.springframework.ide.eclipse.boot.dash.docker.runtarget.DockerImage; +import org.springframework.ide.eclipse.boot.dash.docker.runtarget.DockerRunTarget; +import org.springframework.ide.eclipse.boot.dash.docker.runtarget.DockerRunTargetType; +import org.springframework.ide.eclipse.boot.dash.docker.runtarget.DockerTargetParams; +import org.springframework.ide.eclipse.boot.dash.docker.ui.SelectDockerDaemonDialog.Model; +import org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement; +import org.springframework.ide.eclipse.boot.dash.model.Failable; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.Taggable; +import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.LiveEnvModel; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElement; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.remote.RefreshStateTracker; +import org.springframework.ide.eclipse.boot.dash.model.remote.RemoteJavaLaunchUtil; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTarget.ConnectMode; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction; +import org.springframework.ide.eclipse.boot.dash.views.AddRunTargetAction; +import org.springframework.ide.eclipse.boot.dash.views.BootDashActions; +import org.springframework.ide.eclipse.boot.dash.views.DeleteElementsAction; +import org.springframework.ide.eclipse.boot.dash.views.EnableRemoteDevtoolsAction; +import org.springframework.ide.eclipse.boot.dash.views.RestartDevtoolsClientAction; +import org.springframework.ide.eclipse.boot.dash.views.RunStateAction; +import org.springframework.ide.eclipse.boot.launch.devtools.BootDevtoolsClientLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; +import org.springframework.ide.eclipse.boot.test.util.TestBracketter; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.InspectContainerResponse; +import com.github.dockerjava.api.command.InspectImageResponse; +import com.github.dockerjava.api.model.Container; +import com.github.dockerjava.api.model.Image; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +public class BootDashDockerTests { + + private static final int BUILD_IMAGE_TIMEOUT = 30_000; + private static final String DEFAULT_DOCKER_URL = "unix:///var/run/docker.sock"; + +// @Rule public LaunchCleanups launches = new LaunchCleanups(); + @Rule public TestBracketter bracks = new TestBracketter(); + + @Test + public void testCreateDockerTarget() throws Exception { + createDockerTarget(); + } + + @Test + public void devtoolsFullScenario() throws Exception { + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0"), + withStarters("devtools") + ); + + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + + EnableRemoteDevtoolsAction disableDevtools = actions().getEnableDevtoolsAction(); + + harness.selection.setElements(con); + assertFalse(disableDevtools.isVisible()); + harness.selection.setElements(img); + assertFalse(disableDevtools.isVisible()); + + harness.selection.setElements(dep); + assertTrue(disableDevtools.isVisible()); + assertTrue(disableDevtools.isEnabled()); + assertEquals("Disable Remote DevTools Server", disableDevtools.getText()); + + assertNotNull(getDevtoolsSecret(dep)); + + RestartDevtoolsClientAction restartClient = actions().getRestartDevtoolsClientAction(); + harness.selection.setElements(dep); + assertFalse(restartClient.isVisible()); + harness.selection.setElements(img); + assertFalse(restartClient.isVisible()); + harness.selection.setElements(con); + assertTrue(restartClient.isVisible()); + assertTrue(restartClient.isEnabled()); + + Color grey = BootDashLabels.colorGrey(); + Color green = BootDashLabels.colorGreen(); + + ACondition.waitFor("devtools client started / icons", 5_000, () -> { + assertNotNull(dep.getRunStateImageDecoration()); + assertNotNull(img.getRunStateImageDecoration()); + assertNotNull(con.getRunStateImageDecoration()); + }); + harness.assertLabelContains("devtools", green, dep); // green means... hasdependency and has secret. + harness.assertLabelContains("devtools", grey, img); // grey means... hasdependency but not secret. + harness.assertLabelContains("devtools", green, con); + + { + ILaunch firstLaunch = assertActiveDevtoolsClientLaunch(con); + assertTrue(firstLaunch.canTerminate()); + firstLaunch.terminate(); + ACondition.waitFor("client termination reflected in icons", 5_000, () -> { + assertTrue(firstLaunch.isTerminated()); + assertNull(dep.getRunStateImageDecoration()); + assertNull(img.getRunStateImageDecoration()); + assertNull(con.getRunStateImageDecoration()); + }); + } + + harness.selection.setElements(con); + restartClient.run(); + ACondition.waitFor("active devtools client", 10_000, () -> { + assertActiveDevtoolsClientLaunch(con); + assertNotNull(dep.getRunStateImageDecoration()); + assertNotNull(img.getRunStateImageDecoration()); + assertNotNull(con.getRunStateImageDecoration()); + + harness.assertLabelContains("devtools", green, dep); + harness.assertLabelContains("devtools", grey, img); + harness.assertLabelContains("devtools", green, con); + }); + ILaunch launch = assertActiveDevtoolsClientLaunch(con); + try { + ILaunchConfiguration conf = launch.getLaunchConfiguration(); + + String deploymentSecret = getDevtoolsSecret(dep); + String containerSecret = getDevtoolsSecret(con); + assertNotNull(deploymentSecret); + assertEquals(containerSecret, BootDevtoolsClientLaunchConfigurationDelegate.getRemoteSecret(conf)); + + createFile(project, "src/main/java/com/example/demo/HelloController.java", helloController("Good")); + ACondition.waitFor("Good controller", 15_000, () -> { + String url = con.getUrl(); + assertEquals("Good", IOUtils.toString(new URI(url), "UTF8")); + }); + + createFile(project, "src/main/java/com/example/demo/HelloController.java", helloController("Better")); + ACondition.waitFor("Better controller", 15_000, () -> { + String url = con.getUrl(); + assertEquals("Better", IOUtils.toString(new URI(url), "UTF8")); + }); + + harness.selection.setElements(con); + assertFalse(disableDevtools.isVisible()); + harness.selection.setElements(img); + assertFalse(disableDevtools.isVisible()); + harness.selection.setElements(dep); + assertTrue(disableDevtools.isVisible()); + assertTrue(disableDevtools.isEnabled()); + + disableDevtools.run(); + disableDevtools.lastOperation.get(); + + assertNull(getDevtoolsSecret(dep)); + + ACondition.waitFor("old container stopped", 5_000, () -> { + assertEquals(RunState.INACTIVE, con.getRunState()); + }); + + GenericRemoteAppElement img2 = waitForChild(dep, d -> d instanceof DockerImage && !d.getName().equals(img.getName())); + GenericRemoteAppElement con2 = waitForChild(img2, d -> d instanceof DockerContainer); + containerSecret = getDevtoolsSecret(con2); + assertNull(containerSecret); + + ACondition.waitFor("second container running", 10_000, () -> { + assertEquals(RunState.RUNNING, con2.getRunState()); + assertEquals(RunState.INACTIVE, con.getRunState()); + }); + + harness.selection.setElements(con2); + assertTrue(restartClient.isVisible()); + assertFalse(restartClient.isEnabled()); + + ACondition.waitFor("devtools client terminated", 10_000, () -> { + assertTrue(launch.isTerminated()); + assertNull(dep.getRunStateImageDecoration()); + assertNull(img.getRunStateImageDecoration()); + assertNull(con.getRunStateImageDecoration()); + assertNull(img2.getRunStateImageDecoration()); + assertNull(con2.getRunStateImageDecoration()); + + // text labels and colors + harness.assertLabelContains("devtools", grey, dep); + harness.assertLabelContains("devtools", grey, img); + harness.assertLabelNotContains("devtools", img2); + harness.assertLabelContains("devtools", green, con); + harness.assertLabelNotContains("devtools", con2); + }); + + } finally { + launch.terminate(); + ACondition.waitFor("launch terminated", 2_000, () -> { + assertTrue(launch.isTerminated()); + }); + } + } + + private static String helloController(String message) { + return "package com.example.demo;\n" + + "\n" + + "import org.springframework.web.bind.annotation.GetMapping;\n" + + "import org.springframework.web.bind.annotation.RestController;\n" + + "\n" + + "@RestController\n" + + "public class HelloController {\n" + + "\n" + + " @GetMapping(\"/\")\n" + + " public String hello() {\n" + + " return \""+message+"\";\n" + + " }\n" + + "}"; + } + + private String getDevtoolsSecret(GenericRemoteAppElement el) { + App data = el.getAppData(); + System.out.println("getDevtoolsSecret data ="+data); + if (data instanceof DockerApp) { + @SuppressWarnings("resource") + DockerApp app = (DockerApp)data; + return app.deployment().getSystemProperties().getOrDefault(DevtoolsUtil.REMOTE_SECRET_PROP, null); + } else if (data instanceof DockerContainer) { + DockerContainer con = (DockerContainer)data; + return con.getSystemProps().getOrDefault(DevtoolsUtil.REMOTE_SECRET_PROP, null); + } else { + return null; + } + } + + @Test + public void dragAndDropAProject() throws Exception { + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + + // PT 174387686 - Disable show/link console context menu and tool bar actions for docker images + AbstractBootDashElementsAction openConsoleAction = actions().getOpenConsoleAction(); + AbstractBootDashElementsAction linkWithConsoleAction = actions().getLinkWithConsoleAction(); + harness.selection.setElements(dep); + assertTrue(openConsoleAction.isVisible()); + assertTrue(openConsoleAction.isEnabled()); + assertTrue(linkWithConsoleAction.isVisible()); + assertTrue(linkWithConsoleAction.isEnabled()); + + harness.selection.setElements(img); + assertFalse(openConsoleAction.isVisible()); + assertFalse(openConsoleAction.isEnabled()); + assertFalse(linkWithConsoleAction.isVisible()); + assertFalse(linkWithConsoleAction.isEnabled()); + + harness.selection.setElements(con); + assertTrue(openConsoleAction.isVisible()); + assertTrue(openConsoleAction.isEnabled()); + assertTrue(linkWithConsoleAction.isVisible()); + assertTrue(linkWithConsoleAction.isEnabled()); + + assertConsoleName(dep, "webby - image build output @ unix:///var/run/docker.sock", false); + assertConsoleContains(dep, "Successfully built image 'docker.io/library/webby"); + assertConsoleContains(dep, "Starting WebbyApplication"); + + assertNoConsole(img); + + assertConsoleName(con, "webby - in container "+con.getStyledName(null).getString()+ " @ unix:///var/run/docker.sock", true); + ACondition.waitFor("expected output in container", 5_000, () -> { + assertConsoleContains(con, "Starting WebbyApplication"); + assertConsoleNotContains(con, "Successfully built image 'docker.io/library/webby"); + }); + + assertFalse(harness.getLabel(dep).contains("devtools")); + assertFalse(harness.getLabel(img).contains("devtools")); + assertFalse(harness.getLabel(con).contains("devtools")); + + verifyNoMoreInteractions(ui()); + } + + @Test + public void consoleStopAndStartContainer() throws Exception { + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + + ACondition.waitFor("consoel output from starting app", 5_000, () -> { + assertConsoleContains(dep, "Successfully built image 'docker.io/library/webby"); + assertConsoleContains(dep, "Starting WebbyApplication"); + assertNoConsole(img); + assertConsoleName(con, "webby - in container "+con.getStyledName(null).getString()+ " @ unix:///var/run/docker.sock", true); + assertConsoleContains(con, "Starting WebbyApplication"); + assertConsoleNotContains(con, "Successfully built image 'docker.io/library/webby"); + }); + + clearConsole(con); + clearConsole(dep); + + RunStateAction stopAction = stopAction(); + harness.selection.setElements(con); + stopAction.run(); + + ACondition.waitFor("all stopped", 5_000, () -> { + assertEquals(RunState.INACTIVE, dep.getRunState()); + assertEquals(RunState.INACTIVE, img.getRunState()); + assertEquals(RunState.INACTIVE, con.getRunState()); + }); + + assertConsoleContains(con, "[extShutdownHook]"); + assertConsoleNotContains(con, "Starting WebbyApplication"); + assertNoConsole(img); + assertConsoleContains(dep, "[extShutdownHook]"); + assertConsoleNotContains(dep, "Starting WebbyApplication"); + + RunStateAction startAction = restartAction(); + harness.selection.setElements(con); + startAction.run(); + + ACondition.waitFor("all started", 15_000, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + + ACondition.waitFor("output from restarting app", 5_000, () -> { + assertConsoleNotContains(dep, "Successfully built image 'docker.io/library/webby"); + assertConsoleNotContains(dep, "Starting WebbyApplication"); + assertNoConsole(img); + assertConsoleName(con, "webby - in container "+con.getStyledName(null).getString()+ " @ unix:///var/run/docker.sock", true); + assertConsoleNotContains(con, "Successfully built image 'docker.io/library/webby"); + assertConsoleContains(con, "Starting service [Tomcat]"); + }); + } + + private void clearConsole(GenericRemoteAppElement element) throws Exception { + CloudAppLogManager logManager = context.injections.getBean(CloudAppLogManager.class); + ApplicationLogConsole console = logManager.getExisitingConsole(element); + assertNotNull("No console for " + element.getStyledName(null).getString(), console); + console.clearConsole(); + ACondition.waitFor("clear console", 5_000, () -> { + assertEquals("", console.getDocument().get().trim()); + }); + } + + + private void assertConsoleName(GenericRemoteAppElement element, String expectedName, boolean create) throws Exception { + CloudAppLogManager logManager = context.injections.getBean(CloudAppLogManager.class); + ApplicationLogConsole console = create + ? logManager.getOrCreateConsole(element) + : logManager.getExisitingConsole(element); + assertNotNull("No console for " + element.getStyledName(null).getString(), console); + assertEquals(expectedName, console.getName()); + } + + private void assertConsoleContains(GenericRemoteAppElement element, String expectSnippet) { + CloudAppLogManager logManager = context.injections.getBean(CloudAppLogManager.class); + ApplicationLogConsole console = logManager.getExisitingConsole(element); + assertNotNull("No console for " + element.getStyledName(null).getString(), console); + String text = console.getDocument().get(); + assertContains(expectSnippet, text); + } + + private void assertConsoleNotContains(GenericRemoteAppElement element, String expectSnippet) { + CloudAppLogManager logManager = context.injections.getBean(CloudAppLogManager.class); + ApplicationLogConsole console = logManager.getExisitingConsole(element); + assertNotNull("No console for " + element.getStyledName(null).getString(), console); + String text = console.getDocument().get(); + assertNotContains(expectSnippet, text); + } + + private void assertNoConsole(GenericRemoteAppElement element) { + CloudAppLogManager logManager = context.injections.getBean(CloudAppLogManager.class); + ApplicationLogConsole console = logManager.getExisitingConsole(element); + assertNull("Console for " + element.getStyledName(null).getString() + " exists!", console); + } + + @Test + public void deployAndDebugOnTarget() throws Exception { + RemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + + BootProjectDashElement localElement = harness.waitForElement(2_000, project); + DeployToRemoteTargetAction a = debugOnDockerAction(); + harness.selection.setElements(localElement); + assertTrue(a.isEnabled()); + a.run(); + + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("all debugging", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.DEBUGGING, dep.getRunState()); + assertEquals(RunState.DEBUGGING, img.getRunState()); + assertEquals(RunState.DEBUGGING, con.getRunState()); + }); + ACondition.waitFor("remote debug launch", 5_000, () -> assertActiveDebugLaunch(con)); + + + ILaunch launch = assertActiveDebugLaunch(con); + ILaunchConfiguration conf = launch.getLaunchConfiguration(); + assertEquals(IJavaLaunchConfigurationConstants.ID_REMOTE_JAVA_APPLICATION, conf.getType().getIdentifier()); + + assertEquals(con.getStyledName(null).toString(), conf.getName()); + ACondition.waitFor("launch can terminate", 2_000, () -> { + assertTrue(launch.canTerminate()); + }); + launch.terminate(); + ACondition.waitFor("launch termination", 5_000, () -> { + assertTrue(launch.isTerminated()); + }); + + assertEquals(ImmutableSet.of(conf), con.getLaunchConfigs()); + + ACondition.waitFor("all stopped", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.INACTIVE, dep.getRunState()); + assertEquals(RunState.INACTIVE, img.getRunState()); + assertEquals(RunState.INACTIVE, con.getRunState()); + }); + } + + @Test + public void devtoolsAndDebug() throws Exception { + RemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0"), + withStarters("devtools")); + + BootProjectDashElement localElement = harness.waitForElement(2_000, project); + DeployToRemoteTargetAction a = debugOnDockerAction(); + harness.selection.setElements(localElement); + assertTrue(a.isEnabled()); + a.run(); + + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("all debugging", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.DEBUGGING, dep.getRunState()); + assertEquals(RunState.DEBUGGING, img.getRunState()); + assertEquals(RunState.DEBUGGING, con.getRunState()); + }); + ACondition.waitFor("remote debug launch", 5_000, () -> assertActiveDebugLaunch(con)); + + InspectContainerResponse containerInfo = client().inspectContainerCmd(con.getName()).exec(); + String JAVA_OPTS = getEnv(containerInfo, "JAVA_OPTS"); + + String jmxPort = containerInfo.getConfig().getLabels().get(DockerApp.JMX_PORT); + String debugPort = containerInfo.getConfig().getLabels().get(DockerApp.DEBUG_PORT); + String devtoolsSecret = getDevtoolsSecret(con); + assertNotNull(devtoolsSecret); + assertEquals( + "-Dcom.sun.management.jmxremote.ssl=false "+ + "-Dcom.sun.management.jmxremote.authenticate=false "+ + "-Dcom.sun.management.jmxremote.port="+jmxPort+" "+ + "-Dcom.sun.management.jmxremote.rmi.port="+jmxPort+" "+ + "-Djava.rmi.server.hostname=localhost "+ + "-Dcom.sun.management.jmxremote.local.only=false "+ + "-Dspring.jmx.enabled=true "+ + "-Dspring.application.admin.enabled=true "+ + "-Xdebug "+ + "-Xrunjdwp:server=y,transport=dt_socket,suspend=n,address=*:"+debugPort+" "+ + "-Dspring.devtools.remote.secret="+devtoolsSecret, + + JAVA_OPTS + ); + + ILaunch launch = assertActiveDebugLaunch(con); + ILaunchConfiguration conf = launch.getLaunchConfiguration(); + assertEquals(IJavaLaunchConfigurationConstants.ID_REMOTE_JAVA_APPLICATION, conf.getType().getIdentifier()); + + assertEquals(con.getStyledName(null).toString(), conf.getName()); + ACondition.waitFor("launch can terminate", 2_000, () -> { + assertTrue(launch.canTerminate()); + }); + launch.terminate(); + ACondition.waitFor("launch termination", 5_000, () -> { + assertTrue(launch.isTerminated()); + }); + + assertEquals(ImmutableSet.of(conf), con.getLaunchConfigs()); + + ACondition.waitFor("all stopped", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.INACTIVE, dep.getRunState()); + assertEquals(RunState.INACTIVE, img.getRunState()); + assertEquals(RunState.INACTIVE, con.getRunState()); + }); + } + + + + private String getEnv(InspectContainerResponse imageInspect, String name) { + String[] envs = imageInspect.getConfig().getEnv(); + for (String string : envs) { + if (string.startsWith(name+"=")) { + return string.substring(name.length()+1); + } + } + return null; + } + + @Test + public void deleteRunningContainer() throws Exception { + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + + verifyNoMoreInteractions(ui()); + Mockito.reset(ui()); + + String containerId = con.getName(); + assertEquals(1, listContainersWithId(containerId).size()); + +// when(ui().confirmOperation(eq("Deleting Elements"), any())).thenAnswer(answer); + DeleteElementsAction delete = actions().getDeleteAppsAction(); + harness.selection.setElements(con); + assertTrue(delete.isEnabled()); + delete.run(); + + ACondition.waitFor("container deleted", 5_000, () -> { + assertTrue(img.getChildren().getValue().isEmpty()); + assertEquals(0, listContainersWithId(containerId).size()); + }); + verifyNoMoreInteractions(ui()); + } + + private List listContainersWithId(String containerId) + throws Exception { + return + client().listContainersCmd() + .withShowAll(true) + .withIdFilter(ImmutableList.of(containerId)) + .withLabelFilter(ImmutableList.of(DockerApp.APP_NAME)) + .exec(); + } + + @Test + public void stopAppCurrentSession() throws Exception { + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + verifyNoMoreInteractions(ui()); + reset(ui()); + + RunStateAction stop = stopAction(); + harness.selection.setElements(dep); + assertTrue(stop.isEnabled()); + stop.run(); + + ACondition.waitFor("all started", 5_000, () -> { + assertEquals(RunState.INACTIVE, dep.getRunState()); + assertEquals(RunState.INACTIVE, img.getRunState()); + assertEquals(RunState.INACTIVE, con.getRunState()); + }); + } + + @Test + public void stopAppPreviousSession() throws Exception { + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + String firstSession; + + { + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + verifyNoMoreInteractions(ui()); + reset(ui()); + + firstSession = getSessionId(model); + } + harness.reload(); + { + RemoteBootDashModel model = (RemoteBootDashModel) harness.getRunTargetModel(DockerRunTargetType.class); + + assertFalse(firstSession.equals(getSessionId(model))); + + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + ACondition.waitFor("all started", 5_000, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + RunStateAction stop = stopAction(); + harness.selection.setElements(dep); + assertTrue(stop.isEnabled()); + stop.run(); + + ACondition.waitFor("all stopped", 5_000, () -> { + assertEquals(RunState.INACTIVE, dep.getRunState()); + assertEquals(RunState.INACTIVE, img.getRunState()); + assertEquals(RunState.INACTIVE, con.getRunState()); + }); + } + } + + @Test + public void liveBeans() throws Exception { + IProject project = projects.createBootProject("webby-actuator", + bootVersionAtLeast("2.3.0"), + withStarters("web", "actuator") + ); + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + GenericRemoteAppElement[] nodes = { + con, img, dep + }; + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + verifyNoMoreInteractions(ui()); + + String jmxUrl = con.getJmxUrl(); + ACondition.waitFor("live beans model", 5_000, () -> { + for (GenericRemoteAppElement node : nodes) { + assertEquals(ImmutableSet.of(jmxUrl), node.getActuatorUrls().getValue()); + LiveBeansModel beans = node.getLiveBeans().orElse(null); + assertNotNull(beans); + assertFalse(beans.getBeans().isEmpty()); + } + }); + + RunStateAction stop = stopAction(); + harness.selection.setElements(dep); + stop.run(); + + ACondition.waitFor("Container stopped", 15_000, () -> { //Sometimes stopping container takes a long time. Not sure why. + assertEquals(RunState.INACTIVE, con.getRunState()); + }); + ACondition.waitFor("live beans gone", 5_000, () -> { + for (GenericRemoteAppElement node : nodes) { + assertTrue(node.getActuatorUrls().getValue().isEmpty()); + assertNull(node.getLiveBeans().orElse(null)); + } + }); + } + + @Test + public void liveRequestMappings() throws Exception { + IProject project = projects.createBootProject("webby-actuator", + bootVersionAtLeast("2.3.0"), + withStarters("web", "actuator") + ); + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + GenericRemoteAppElement[] nodes = { + con, img, dep + }; + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + verifyNoMoreInteractions(ui()); + + String jmxUrl = con.getJmxUrl(); + ACondition.waitFor("live requestmappings", 5_000, () -> { + for (GenericRemoteAppElement node : nodes) { + assertEquals(ImmutableSet.of(jmxUrl), node.getActuatorUrls().getValue()); + List rm = node.getLiveRequestMappings().orElse(null); + assertNotNull(rm); + assertFalse(rm.isEmpty()); + } + }); + + RunStateAction stop = stopAction(); + harness.selection.setElements(dep); + stop.run(); + + ACondition.waitFor("Container stopped", 15_000, () -> { //Sometimes stopping container takes a long time. Not sure why. + assertEquals(RunState.INACTIVE, con.getRunState()); + }); + ACondition.waitFor("live requestmappings gone", 5_000, () -> { + for (GenericRemoteAppElement node : nodes) { + assertTrue(node.getActuatorUrls().getValue().isEmpty()); + assertNull(node.getLiveBeans().orElse(null)); + } + }); + } + + @Test + public void liveDataNotAvailable() throws Exception { + IProject project = projects.createBootProject("webby-actuator", + bootVersionAtLeast("2.3.0"), + withStarters("web") + ); + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + GenericRemoteAppElement[] nodes = { + con, img, dep + }; + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + verifyNoMoreInteractions(ui()); + + ACondition.waitFor("live requestmappings", 5_000, () -> { + for (GenericRemoteAppElement node : nodes) { + Failable> rm = node.getLiveRequestMappings(); + Failable beans = node.getLiveBeans(); + Failable env = node.getLiveEnv(); + + assertTrue(rm.hasFailed()); + assertContains("Enable actuator endpoint mappings", rm.getErrorMessage().toHtml()); + + assertTrue(beans.hasFailed()); + assertContains("Enable actuator endpoint beans", beans.getErrorMessage().toHtml()); + + assertTrue(env.hasFailed()); + assertContains("Enable actuator endpoint env", env.getErrorMessage().toHtml()); + } + }); + + RunStateAction stop = stopAction(); + harness.selection.setElements(dep); + stop.run(); + + ACondition.waitFor("Container stopped", 15_000, () -> { //Sometimes stopping container takes a long time. Not sure why. + assertEquals(RunState.INACTIVE, con.getRunState()); + }); + ACondition.waitFor("live requestmappings gone", 5_000, () -> { + for (GenericRemoteAppElement node : nodes) { + assertTrue(node.getActuatorUrls().getValue().isEmpty()); + assertNull(node.getLiveBeans().orElse(null)); + } + }); + } + + @Test + public void liveEnv() throws Exception { + IProject project = projects.createBootProject("webby-actuator", + bootVersionAtLeast("2.3.0"), + withStarters("web", "actuator") + ); + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + GenericRemoteAppElement[] nodes = { + con, img, dep + }; + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + verifyNoMoreInteractions(ui()); + + String jmxUrl = con.getJmxUrl(); + ACondition.waitFor("live env", 5_000, () -> { + for (GenericRemoteAppElement node : nodes) { + assertEquals(ImmutableSet.of(jmxUrl), node.getActuatorUrls().getValue()); + LiveEnvModel env = node.getLiveEnv().orElse(null); + assertNotNull(env); + assertFalse(env.getPropertySources().getPropertySources().isEmpty()); + } + }); + + RunStateAction stop = stopAction(); + harness.selection.setElements(dep); + stop.run(); + + ACondition.waitFor("Container stopped", 15_000, () -> { //Sometimes stopping container takes a long time. Not sure why. + assertEquals(RunState.INACTIVE, con.getRunState()); + }); + ACondition.waitFor("live env gone", 5_000, () -> { + for (GenericRemoteAppElement node : nodes) { + assertTrue(node.getActuatorUrls().getValue().isEmpty()); + assertNull(node.getLiveEnv().orElse(null)); + } + }); + } + + @Test + public void dragAndDropGradleProject() throws Exception { + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0"), withImportStrategy("GRADLE")); + + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + + verifyNoMoreInteractions(ui()); + } + + @Test + public void tagsPersistence() throws Exception { + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + + { + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + assertTrue("Tags empty initally", dep.getTags().isEmpty()); + assertTrue("Tags empty initally", img.getTags().isEmpty()); + assertTrue("Tags empty initally", con.getTags().isEmpty()); + + setTags(dep, "tag1", "tag2"); + assertTags(dep, "tag1", "tag2"); + harness.assertLabelContains("[tag1, tag2]", dep); + } + + harness.reload(); + + { + RemoteBootDashModel model = (RemoteBootDashModel) harness + .getRunTargetModel(DockerRunTargetType.class); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + assertTags(dep, "tag1", "tag2"); + assertTrue("Should be still empty", img.getTags().isEmpty()); + assertTrue("Should be still empty", con.getTags().isEmpty()); + harness.assertLabelContains("[tag1, tag2]", dep); + } + + } + + @Test + public void urlComputation() throws Exception { + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + img.setDefaultRequestMappingPath("/hello"); + + ACondition.waitFor("Wait for port to be defined", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(1, con.getLivePorts().size()); + assertEquals(1, img.getLivePorts().size()); + assertEquals(1, dep.getLivePorts().size()); + }); + + int port = con.getLivePort(); + assertEquals(ImmutableSet.of(port), img.getLivePorts()); + assertEquals(ImmutableSet.of(port), dep.getLivePorts()); + assertEquals(ImmutableSet.of(port), con.getLivePorts()); + + assertEquals("http" + "://localhost:" + port + "/", dep.getUrl()); + assertEquals("http" + "://localhost:" + port + "/hello", img.getUrl()); + + harness.assertLabelContains("/hello", img); + + } + + @Test + public void instanceCount() throws Exception { + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("Previous container stopped and deployment started", BUILD_IMAGE_TIMEOUT, () -> { + assertTrue(dep.getRunState() == RunState.RUNNING); + assertEquals(1, dep.getDesiredInstances()); + assertEquals(-1, img.getDesiredInstances()); + assertEquals(-1, con.getDesiredInstances()); + assertEquals(1, dep.getActualInstances()); + assertEquals(1, img.getActualInstances()); + assertEquals(1, con.getActualInstances()); + }); + + harness.selection.setElements(dep); + restartAction().run(); + + ACondition.waitFor("Second container to appear", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(2, img.getChildren().getValues().size()); + }); + + String id = con.getName(); + + GenericRemoteAppElement con2 = getChild(img, d -> d instanceof DockerContainer && !id.equals(d.getName())); + + ACondition.waitFor("Previous container stopped and deployment started", BUILD_IMAGE_TIMEOUT, () -> { + assertTrue(con.getRunState() == RunState.INACTIVE); + assertTrue(con2.getRunState() == RunState.RUNNING); + // No diffs in project hence same image but another container started + assertEquals(1, dep.getChildren().getValues().size()); + + assertEquals(1, dep.getDesiredInstances()); + assertEquals(-1, img.getDesiredInstances()); + assertEquals(-1, con.getDesiredInstances()); + assertEquals(-1, con2.getDesiredInstances()); + assertEquals(1, dep.getActualInstances()); + assertEquals(1, img.getActualInstances()); + assertEquals(0, con.getActualInstances()); + assertEquals(1, con2.getActualInstances()); + + }); + + harness.assertInstancesLabel("1/1", "", dep); + harness.assertInstancesLabel("1", "", img); + harness.assertInstancesLabel("0", "", con); + harness.assertInstancesLabel("1", "", con2); + + harness.selection.setElements(con); + restartAction().run(); + + ACondition.waitFor("Both containers running", BUILD_IMAGE_TIMEOUT, () -> { + assertTrue(con.getRunState() == RunState.RUNNING); + assertTrue(con2.getRunState() == RunState.RUNNING); + // No diffs in project hence same image but another container started + assertEquals(1, dep.getChildren().getValues().size()); + + assertEquals(1, dep.getDesiredInstances()); + assertEquals(-1, img.getDesiredInstances()); + assertEquals(-1, con.getDesiredInstances()); + assertEquals(-1, con2.getDesiredInstances()); + assertEquals(2, dep.getActualInstances()); + assertEquals(2, img.getActualInstances()); + assertEquals(1, con.getActualInstances()); + assertEquals(1, con2.getActualInstances()); + + }); + + harness.assertInstancesLabel("2/1", "2/1", dep); + harness.assertInstancesLabel("2", "2", img); + harness.assertInstancesLabel("1", "", con); + harness.assertInstancesLabel("1", "", con2); + } + + @Test + public void deleteDeployment() throws Exception { + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + + verifyNoMoreInteractions(ui()); + Mockito.reset(ui()); + + String imageId = ((DockerImage)img.getAppData()).getName(); + + DeleteElementsAction deleteAction = actions().getDeleteAppsAction(); + assertTrue(harness.selection.getElements().isEmpty()); + assertFalse(deleteAction.isEnabled()); + harness.selection.setElements(dep); + assertTrue(deleteAction.isEnabled()); + + //if (ui().confirmOperation("Deleting Elements", modifiable.getDeletionConfirmationMessage(workitem.getValue()))) { + +// confirm popup was disabled. +// when(ui().confirmOperation("Deleting Elements", "Delete webby ?")).thenReturn(true); + deleteAction.run(); + + ACondition.waitFor("Everything is deleted", 5_000, () -> { + assertTrue(model.getElements().getValues().isEmpty()); // deployment node disapear from model + assertNoImage(imageId); + + assertTrue( + client().listContainersCmd().withShowAll(true).exec() + .isEmpty() + ); + +// client().listImages(ListImagesParam.allImages()).stream(). + }); + } + + @Test + public void deleteRunningImage() throws Exception { + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + + dragAndDrop(project, model); + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + String imgId = img.getName(); + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + + DeleteElementsAction delete = actions().getDeleteAppsAction(); + harness.selection.setElements(img); + assertTrue(delete.isEnabled()); + assertTrue(delete.isVisible()); + + delete.run(); + + ACondition.waitFor("Image and container deletion", 10_000, () -> { + assertEquals(RunState.INACTIVE, dep.getRunState()); + assertTrue(dep.getChildren().getValues().isEmpty()); + assertTrue(img.isDisposed()); + assertTrue(con.isDisposed()); + + assertNoImage(imgId); + assertTrue( + client().listContainersCmd().withShowAll(true).exec() + .isEmpty() + ); + }); + } + + @Test + public void noAutoStartForMismatchingSession() throws Exception { + GenericRemoteBootDashModel model = createDockerTarget(); + Mockito.reset(ui()); + IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0")); + + dragAndDrop(project, model); + { + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, dep.getRunState()); + assertEquals(RunState.RUNNING, img.getRunState()); + assertEquals(RunState.RUNNING, con.getRunState()); + }); + + verifyNoMoreInteractions(ui()); + Mockito.reset(ui()); + + String sessionId = getSessionId(model); + assertNotNull(sessionId); + assertEquals(sessionId, getSessionId(dep)); + + String containerId = con.getName(); + client().stopContainerCmd(containerId).withTimeout(2).exec(); + ACondition.waitFor("Container stopped", 5_000, () -> { + InspectContainerResponse info = client().inspectContainerCmd(containerId).exec(); + String status = info.getState().getStatus(); + System.out.println("status = "+status); + assertEquals("exited", status); + }); + } + + model.disconnect(); + + ACondition.waitFor("Docker app dispaeared", 2_000, () -> { + assertTrue(model.getElements().getValues().isEmpty()); + }); + + CompletableFuture deploymentSynchronized = RefreshStateTracker.waitForOperation("Synchronizing deployment "+project.getName()); + model.connect(ConnectMode.INTERACTIVE); + + GenericRemoteAppElement dep = waitForDeployment(model, project); + GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage); + GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer); + + deploymentSynchronized.get(1, TimeUnit.SECONDS); + + assertEquals(RunState.INACTIVE, dep.getRunState()); + assertEquals(RunState.INACTIVE, img.getRunState()); + assertEquals(RunState.INACTIVE, con.getRunState()); + } + + @Test + public void canceledDockerTargetCreation() throws Exception { + DockerRunTargetType target = injections().getBean(DockerRunTargetType.class); + AddRunTargetAction createTarget = getCreateTargetAction(target); + assertNotNull(createTarget); + assertTrue(createTarget.isEnabled()); + + doAnswer(invocation -> { + Model model = (Model) invocation.getArguments()[0]; + //model.performOk(); //no clicking ok, so that's like when dialog is 'canceled'. + return null; + }).when(ui()).selectDockerDaemonDialog(Matchers.any()); + + createTarget.run(); + createTarget.waitFor(Duration.ofMillis(2000)); + + assertTrue(harness.getRunTargetModels(target).isEmpty()); + } + + ////////////////////////////////////////////// + /// harness + + @After + public void cleanup() { + RefreshStateTracker.clearDebugObservers(); + } + + private void assertNoLaunchConfigs(String typeId) throws CoreException { + ILaunchManager lm = DebugPlugin.getDefault().getLaunchManager(); + ILaunchConfigurationType type = lm.getLaunchConfigurationType(typeId); + ILaunchConfiguration[] confs = lm.getLaunchConfigurations(type); + assertTrue(confs == null || confs.length == 0); + } + + private void assertNoActiveDevtoolsClientLaunch(GenericRemoteAppElement el) throws CoreException { + List launches = getDevtoolsClientLaunches(el); + assertTrue(launches.isEmpty()); + } + + private ILaunch assertActiveDevtoolsClientLaunch(GenericRemoteAppElement el) throws CoreException { + List launches = getDevtoolsClientLaunches(el); + assertEquals(1, launches.size()); + return launches.get(0); + } + + private List getDevtoolsClientLaunches(GenericRemoteAppElement el) throws CoreException { + List launches = new ArrayList<>(); + for (ILaunch l : DebugPlugin.getDefault().getLaunchManager().getLaunches()) { + if (!l.isTerminated()) { + ILaunchConfiguration conf = l.getLaunchConfiguration(); + if (conf.getType().getIdentifier().equals(BootDevtoolsClientLaunchConfigurationDelegate.TYPE_ID)) { + String url = DevtoolsUtil.remoteUrl(el); + assertTrue(url.startsWith("http")); + if (url.equals(conf.getAttribute(BootDevtoolsClientLaunchConfigurationDelegate.REMOTE_URL, ""))) { + launches.add(l); + } + } + } + } + return launches; + } + + private ILaunch assertActiveDebugLaunch(GenericRemoteAppElement el) throws CoreException { + DockerContainer container = (DockerContainer)el.getAppData(); + String containerId = container.getName(); + List launches = new ArrayList<>(); + for (ILaunch l : DebugPlugin.getDefault().getLaunchManager().getLaunches()) { + if (!l.isTerminated()) { + ILaunchConfiguration conf = l.getLaunchConfiguration(); + if (conf!=null && containerId.equals(conf.getAttribute(RemoteJavaLaunchUtil.APP_NAME, ""))) { + launches.add(l); + } + } + } + assertEquals(1, launches.size()); + return launches.get(0); + } + + private String getSessionId(GenericRemoteAppElement dep) { + App data = dep.getAppData(); + assertTrue(data instanceof DockerApp); + return ((DockerApp)data).deployment().getSessionId(); + } + + private String getSessionId(RemoteBootDashModel model) { + DockerRunTarget target = (DockerRunTarget) model.getRunTarget(); + return target.getSessionId(); + } + + private GenericRemoteBootDashModel createDockerTarget() throws Exception { + DockerRunTargetType target = injections().getBean(DockerRunTargetType.class); + AddRunTargetAction createTarget = getCreateTargetAction(target); + assertNotNull(createTarget); + assertTrue(createTarget.isEnabled()); + + doAnswer(invocation -> { + Model model = (Model) invocation.getArguments()[0]; + model.performOk(); + return null; + }).when(ui()).selectDockerDaemonDialog(Matchers.any()); + + createTarget.run(); + createTarget.waitFor(Duration.ofMillis(5000)); + + BootDashModel model; + // ACondition.waitFor("Run target model to appear", 2000, () -> { + assertNotNull(model = harness.getRunTargetModel(target)); +// }); + return (GenericRemoteBootDashModel) model; + } + + private void assertNoImage(String imageId) throws Exception { + for (Image img : client().listImagesCmd().withShowAll(true).exec()) { + assertFalse(imageId.equals(img.getId())); + } + } + + private GenericRemoteAppElement waitForChild(GenericRemoteAppElement dep, Predicate selector) throws Exception { + ACondition.waitFor("node to appear", 120_000, () -> { + getChild(dep, selector); + }); + return getChild(dep, selector); + } + + private GenericRemoteAppElement getChild(GenericRemoteAppElement node, Predicate selector) throws Exception { + List selected = new ArrayList<>(); + for (BootDashElement _child : node.getChildren().getValues()) { + if (_child instanceof GenericRemoteAppElement) { + GenericRemoteAppElement child = (GenericRemoteAppElement) _child; + App data = child.getAppData(); + if (selector.test(data)) { + selected.add(child); + } + } + } + assertEquals(1, selected.size()); + return selected.get(0); + } + + private GenericRemoteAppElement waitForDeployment(RemoteBootDashModel model, + IProject project) throws Exception { + ACondition.waitFor("project deployment node", 2_000, () -> { + getDeployment(model, project); + }); + GenericRemoteAppElement d = getDeployment(model, project); + return d; + } + + DockerClient _client; + + private DockerClient client() { + if (_client==null) { + _client = DockerRunTargetType.createDockerClient(DEFAULT_DOCKER_URL); + } + return _client; + } + + @After + public void tearDown() throws Exception { + try { + List cons = client().listContainersCmd() + .withShowAll(true) + .withLabelFilter(ImmutableList.of(DockerApp.APP_NAME)) + .exec(); + //Delete all 'our' containers + for (Container c : cons) { + String label = c.getLabels().getOrDefault(DockerApp.APP_NAME, ""); + assertTrue(StringUtil.hasText(label)); + System.out.println("removing container: "+c.getId()); + client().removeContainerCmd(c.getId()).withForce(true).withRemoveVolumes(true).exec(); + } + //Delete all dangling images + for (Image img : client().listImagesCmd().withDanglingFilter(true).exec()) { + System.out.println("removing image: "+img.getId()); + client().removeImageCmd(img.getId()).withForce(true).withNoPrune(false).exec(); + } + } finally { + if (_client!=null) { + _client.close(); + } + } + } + + private GenericRemoteAppElement getDeployment(BootDashModel model, IProject project) { + for (BootDashElement e : model.getElements().getValues()) { + if (project.equals(e.getProject())) { + return (GenericRemoteAppElement) e; + } + } + throw new NoSuchElementException("No element for project "+project.getName()); + } + + + private void dragAndDrop(IProject project, GenericRemoteBootDashModel model) throws Exception { + assertTrue(model.canBeAdded(ImmutableList.of(project))); + model.add(ImmutableList.of(project)); + } + + private TestBootDashModelContext context = new TestBootDashModelContext( + ResourcesPlugin.getWorkspace(), + DebugPlugin.getDefault().getLaunchManager() + ); + { + context.injections.def(DockerRunTargetType.class, DockerRunTargetType::new); + context.injections.defInstance(RunTargetType.class, RunTargetTypes.LOCAL); + } + BootDashViewModelHarness harness = new BootDashViewModelHarness(context); + BootProjectTestHarness projects = new BootProjectTestHarness(ResourcesPlugin.getWorkspace()); + BootDashActions actions; + + private BootDashActions actions() { + if (actions==null) { + actions = new BootDashActions(harness.model, harness.selection.forReading(), injections(), null); + } + return actions; + } + + private SimpleDIContext injections() { + return context.injections; + } + + private AllUserInteractions ui() { + return injections().getBean(AllUserInteractions.class); + } + + private AddRunTargetAction getCreateTargetAction(RunTargetType type) { + for (AddRunTargetAction a : actions().getAddRunTargetActions()) { + if (a.runTargetType.equals(type)) { + return a; + } + } + throw new NoSuchElementException("Add target action not found for "+type); + } + + + private DeployToRemoteTargetAction debugOnDockerAction() { + ImmutableList as = actions().getDebugOnTargetActions(); + for (IAction a : as) { + if (a instanceof DeployToRemoteTargetAction) { + DeployToRemoteTargetAction deployAction = (DeployToRemoteTargetAction) a; + RemoteRunTarget target = ((DeployToRemoteTargetAction) a).getTarget(); + if (DockerRunTargetType.class == target.getType().getClass()) { + return deployAction; + } + } + } + throw new NoSuchElementException("Debug On Docker Target Action not found"); + } + + private RunStateAction runstateAction(RunState state, String expectedLabel) { + for (RunStateAction a : actions().getRunStateActions()) { + if (a.getGoalState() == state) { + assertEquals(expectedLabel, a.getText()); + return a; + } + } + fail("Cannot find runstate action for "+state); + return null; + } + + private RunStateAction stopAction() { + return runstateAction(RunState.INACTIVE, "Stop"); + } + + private RunStateAction restartAction() { + return runstateAction(RunState.RUNNING, "(Re)start"); + } + + private void setTags(Taggable e, String... tags) { + LinkedHashSet tagSet = new LinkedHashSet<>(); + for (String tag : tags) { + tagSet.add(tag); + } + e.setTags(tagSet); + } + + private void assertTags(Taggable e, String... tags) { + LinkedHashSet tagSet = new LinkedHashSet<>(); + for (String tag : tags) { + tagSet.add(tag); + } + assertEquals(tagSet, e.getTags()); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashElementTagsTests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashElementTagsTests.java new file mode 100644 index 000000000..1ff3f85f2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashElementTagsTests.java @@ -0,0 +1,277 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.LinkedHashSet; + +import org.eclipse.core.resources.IProject; +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElementsFilterBoxModel; +import org.springframework.ide.eclipse.boot.dash.model.LocalBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.TagUtils; +import org.springframework.ide.eclipse.boot.dash.test.AbstractLaunchConfigurationsDashElementTest.TestElement; +import org.springframework.ide.eclipse.boot.dash.test.mocks.Mocks; +import org.springframework.ide.eclipse.boot.dash.util.LaunchConfRunStateTracker; + +/** + * Light-weight mockito-based tests for tags. + * + * @author Alex Boyko + * + */ +public class BootDashElementTagsTests extends Mocks { + + private static TestElement createElement(String name, String... tags) { +// LaunchConfDashElementFactory childFactory = mock(LaunchConfDashElementFactory.class); +// BootProjectDashElementFactory factory = mock(BootProjectDashElementFactory.class); +// IScopedPropertyStore projectProperties = new MockScopedPropertyStore(); + IProject project = mockProject(name, true); + LocalBootDashModel model = mock(LocalBootDashModel.class); + LaunchConfRunStateTracker tracker = mock(LaunchConfRunStateTracker.class); + when(model.getLaunchConfRunStateTracker()).thenReturn(tracker); + TestElement element = spy(new TestElement(name, project, model)); + when(element.getTags()).thenReturn(new LinkedHashSet(Arrays.asList(tags))); + return element; + } + + @Test + public void testTagParsing_1() throws Exception { + assertArrayEquals(new String[] {"xd", "spring"}, TagUtils.parseTags("xd,spring")); + } + + @Test + public void testTagParsing_2() throws Exception { + assertArrayEquals(new String[0], TagUtils.parseTags("")); + } + + @Test + public void testTagParsing_3() throws Exception { + assertArrayEquals(new String[0], TagUtils.parseTags(" , ,, , \t, , ,,,\n\t, ")); + } + + @Test + public void testTagParsing_4() throws Exception { + assertArrayEquals(new String[] {"spring"}, TagUtils.parseTags(" spring \t ")); + } + + @Test + public void testTagParsing_5() throws Exception { + assertArrayEquals(new String[] {"spring", "spring"}, TagUtils.parseTags("spring ,\t spring ,")); + } + + @Test + public void testTagParsing_6() throws Exception { + assertArrayEquals(new String[] {"spring"}, TagUtils.parseTags(", ,,, spring")); + } + + @Test + public void defaultValues_FilterTest() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring", "xd"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "springsource"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring source", "xd"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring, source"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "source"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {}))); + } + + @Test + public void empty_FilterTest() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue(""); + + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "springsource"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring source"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring, source"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "source"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {}))); + } + + @Test + public void basicSearchTerm_FilterTest() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue("spring"); + + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "springsource"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring source"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring, source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {}))); + } + + @Test + public void basicSearchTermWithWhiteSpace_FilterTest() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue(" \t , ,,, spring \t \n "); + + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "springsource"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring source"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring, source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {}))); + } + + @Test + public void basicSearchTag_FilterTest() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue("spring,"); + + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "springsource"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring, source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {}))); + } + + @Test + public void basicSearchTagWithWhiteSpace_FilterTest() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue(" \t , ,,, spring \t , \n "); + + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "springsource"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring, source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {}))); + } + + @Test + public void multipleSearchTags_FilterTest() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue("spring,xd,"); + + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "springsource"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring, source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {}))); + } + + @Test + public void mixedCaseSearchTags_FilterTest() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue("sPriNg,xD,"); + + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"Xd", "sprinG"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "springsource"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spRing source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xD", "spring, source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {}))); + } + + @Test + public void multipleSearchTagsWithWhiteSpace_FilterTest() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue(" \t , ,,, spring \n\t, , ,,,, , \t \n, xd \n \t , \t\n, ,,,, , ,"); + + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "springsource"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring, source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {}))); + } + + @Test + public void combineSearchTagsAndTerm_FilterTest() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue("xd,spring"); + + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring", "xd"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "springsource"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring source", "xd"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring, source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {}))); + } + + @Test + public void combineSearchTagsAndTermWithWhiteSpace_FilterTest() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue(" \t , ,,, xd \n\t, , ,,,, , \t \n, spring"); + + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring", "xd"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "springsource"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"spring source", "xd"}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "spring, source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {"xd", "source"}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("t1", new String[] {}))); + } + + @Test + public void filterMatchesElementName() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue("foo"); + + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("demo-foo", new String[] {}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("demo-foo-bar", new String[] {}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("foo", new String[] {}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("demo", new String[] {}))); + } + + @Test + public void starWildcardInSearchTerm() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue("foo*bar"); + + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("something-foo-more-bar", new String[] {}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("demo-bar", new String[] {}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("foobar", new String[] {}))); + } + + @Test + public void questionmarkWildcardInSearchTerm() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue("foo?bar"); + + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("something-foo-bar", new String[] {}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("something-foo-more-bar", new String[] {}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("foobar", new String[] {}))); + } + + @Test + public void wildcardInTag() throws Exception { + BootDashElementsFilterBoxModel filterBoxModel = new BootDashElementsFilterBoxModel(); + filterBoxModel.getText().setValue("foo*bar,"); + + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("something-foo-bar", new String[] {}))); + assertFalse(filterBoxModel.getFilter().getValue().accept(createElement("something-foo-more-bar", new String[] {}))); + assertTrue(filterBoxModel.getFilter().getValue().accept(createElement("foobar", new String[] {}))); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashModelTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashModelTest.java new file mode 100644 index 000000000..91da3adeb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashModelTest.java @@ -0,0 +1,1586 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.contains; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.ide.eclipse.boot.dash.test.actuator.RequestMappingAsserts.assertRequestMappingWithPath; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.bootVersionAtLeast; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.setPackage; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.withStarters; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.assertElements; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.createFile; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.setContents; + +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.io.FileUtils; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.ui.IWorkingSet; +import org.eclipse.ui.IWorkingSetManager; +import org.eclipse.ui.PlatformUI; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +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.MavenId; +import org.springframework.ide.eclipse.boot.core.SpringBootCore; +import org.springframework.ide.eclipse.boot.core.cli.BootCliCommand; +import org.springframework.ide.eclipse.boot.core.cli.BootInstallManager; +import org.springframework.ide.eclipse.boot.core.cli.install.CloudCliInstall; +import org.springframework.ide.eclipse.boot.core.cli.install.IBootInstall; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudServiceInstanceDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElementsFilterBoxModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement; +import org.springframework.ide.eclipse.boot.dash.model.ButtonModel; +import org.springframework.ide.eclipse.boot.dash.model.LaunchConfDashElement; +import org.springframework.ide.eclipse.boot.dash.model.LocalCloudServiceDashElement; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel.FilterChoice; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springframework.ide.eclipse.boot.dash.util.CollectionUtils; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.util.PortFinder; +import org.springframework.ide.eclipse.boot.test.AutobuildingEnablement; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.WizardConfigurer; +import org.springframework.ide.eclipse.boot.test.util.TestBracketter; +import org.springframework.ide.eclipse.boot.util.version.Version; +import org.springsource.ide.eclipse.commons.core.ZipFileUtil; +import org.springsource.ide.eclipse.commons.frameworks.core.maintype.MainTypeFinder; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; +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.util.Filter; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +import com.google.common.collect.ImmutableSet; + +/** + * @author Kris De Volder + */ +public class BootDashModelTest { + + private static final long MODEL_UPDATE_TIMEOUT = 10000; // short, should be nearly instant + private static final long RUN_STATE_CHANGE_TIMEOUT = 40000; + private static final long MAVEN_BUILD_TIMEOUT = 80000; + + private SpringBootCore springBootCore = SpringBootCore.getDefault(); // should be getting this via projects harness? + private TestBootDashModelContext context; + private BootProjectTestHarness projects; + private BootDashModel model; + + private PortFinder portFinder = new PortFinder(); + + @Rule + public AutobuildingEnablement autobuild = new AutobuildingEnablement(false); + + @Rule + public TestBracketter testBracketer = new TestBracketter(); + + static final String ENABLE_CLOUD_CLI_BUTTON = "Install local cloud services"; + + @Rule + public DumpBootProcessOutput processOutput = new DumpBootProcessOutput(); + + + @Test public void testAutoInstallSpringCloudCLI() throws Exception { + assertTrue(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + assertNull(harness.getButton(model, ENABLE_CLOUD_CLI_BUTTON)); + assertTrue(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + model.getViewModel().getToggleFilters().getSelectedFilters().remove(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES); + ACondition.waitFor("Cloud CLI Button to appear", 500, () -> harness.assertButton(model, ENABLE_CLOUD_CLI_BUTTON)); + + ButtonModel button = harness.assertButton(model, ENABLE_CLOUD_CLI_BUTTON); + + when(ui().confirmOperation(contains("Confirm Installation of Spring Cloud CLI"), contains("will be installed into"))).thenReturn(true); + //Actual confirm dialog prompts example: [Confirm Installation of Spring Cloud CLI?, Spring Cloud CLI version 1.3.3.RELEASE will be installed into Boot 1.5.4.RELEASE.] + + button.perform(ui()); + assertFalse(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + ACondition.waitFor("Local services to appear", MAVEN_BUILD_TIMEOUT, () -> { + assertServiceElements("dataflow", "zipkin", "eureka", "kafka", "h2", "configserver", "hystrixdashboard", "stubrunner"); + }); + assertNull(harness.getButton(model, ENABLE_CLOUD_CLI_BUTTON)); + + verify(ui()).confirmOperation(anyString(), anyString()); + verifyNoMoreInteractions(ui()); + reset(ui()); + + // State preserved/restored on reload? + harness.reload(); + model = harness.getRunTargetModel(RunTargetTypes.LOCAL); + + assertServiceElements(/*NONE*/); //TODO: This test is subject to race condition, the elements will appear, we don't know how fast. + //If very fast... then this test will fail! To avoid the race condition we must somehow add a mechanic in the harness to + //to be able to hold-up the stuff that resolves local services until we explicitly allow it to proceed. + assertFalse(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + + ACondition.waitFor("Local services to appear", MAVEN_BUILD_TIMEOUT, () -> { + assertServiceElements("dataflow", "zipkin", "eureka", "kafka", "h2", "configserver", "hystrixdashboard", "stubrunner"); + }); + assertNull(harness.getButton(model, ENABLE_CLOUD_CLI_BUTTON)); + verifyZeroInteractions(ui()); //Since cloud cli already installed we should not be prompted whether we want to install it. + } + + @Test public void testAutoInstallSpringCloudCLIRejected() throws Exception { + assertTrue(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + assertNull(harness.getButton(model, ENABLE_CLOUD_CLI_BUTTON)); + assertTrue(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + model.getViewModel().getToggleFilters().getSelectedFilters().remove(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES); + ACondition.waitFor("Cloud CLI Button to appear", 500, () -> harness.assertButton(model, ENABLE_CLOUD_CLI_BUTTON)); + + ButtonModel button = harness.assertButton(model, ENABLE_CLOUD_CLI_BUTTON); + when(ui().confirmOperation(contains("Confirm Installation of Spring Cloud CLI"), contains("will be installed into"))).thenReturn(false); + button.perform(ui()); + + harness.assertButton(model, ENABLE_CLOUD_CLI_BUTTON); + assertFalse(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + + assertServiceElements(/*NONE*/); + verify(ui()).confirmOperation(anyString(), anyString()); + verifyNoMoreInteractions(ui()); + } + + @Test public void testAutoInstallSpringCloudCLICustom() throws Exception { + //We support installing spring-cloud CLI into a custom configured spring boot cli + String cliVersion = "2.1.4.RELEASE"; // consider updating once in a while to remain 'current'. + URL customZipUrl = new URL("https://repo.spring.io/release/org/springframework/boot/spring-boot-cli/"+cliVersion+"/spring-boot-cli-"+cliVersion+"-bin.zip"); + IBootInstall customCliInstall = performCustomCliInstall(customZipUrl); + + //Some sanity checks... to see if performCustomCliInstall works as expected. + assertEquals("custom-spring-"+cliVersion, customCliInstall.getName()); + assertEquals(cliVersion, customCliInstall.getVersion()); + assertEquals(customCliInstall, context.getBootInstallManager().getDefaultInstall()); + assertTrue(context.getBootInstallManager().getInstalls().contains(customCliInstall)); + + assertTrue(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + assertNull(harness.getButton(model, ENABLE_CLOUD_CLI_BUTTON)); + assertTrue(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + model.getViewModel().getToggleFilters().getSelectedFilters().remove(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES); + ACondition.waitFor("Cloud CLI Button to appear", 500, () -> harness.assertButton(model, ENABLE_CLOUD_CLI_BUTTON)); + + when(ui().confirmOperation(contains("Confirm Installation of Spring Cloud CLI"), contains("will be installed into"))).thenReturn(true); + ButtonModel button = harness.assertButton(model, ENABLE_CLOUD_CLI_BUTTON); + button.perform(ui()); + assertFalse(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + + ACondition.waitFor("Local services to appear", MAVEN_BUILD_TIMEOUT, () -> { + assertServiceElements("dataflow", "zipkin", "eureka", "kafka", "h2", "configserver", "hystrixdashboard", "stubrunner"); + }); + assertNull(harness.getButton(model, ENABLE_CLOUD_CLI_BUTTON)); + + verify(ui()).confirmOperation(anyString(), anyString()); + verifyNoMoreInteractions(ui()); + } + + @Test public void testAutoInstallSpringCloudCLINotSupported() throws Exception { + //Behaves reasonable with ancient versions of boot? + + String cliVersion = "1.0.0.RELEASE"; // Deliberately ancient + URL customZipUrl = new URL("https://repo.spring.io/release/org/springframework/boot/spring-boot-cli/"+cliVersion+"/spring-boot-cli-"+cliVersion+"-bin.zip"); + IBootInstall customCliInstall = performCustomCliInstall(customZipUrl); + + //Some sanity checks... to see if performCustomCliInstall works as expected. + assertEquals("custom-spring-"+cliVersion, customCliInstall.getName()); + assertEquals(cliVersion, customCliInstall.getVersion()); + assertEquals(customCliInstall, context.getBootInstallManager().getDefaultInstall()); + assertTrue(context.getBootInstallManager().getInstalls().contains(customCliInstall)); + + assertTrue(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + assertNull(harness.getButton(model, ENABLE_CLOUD_CLI_BUTTON)); + assertTrue(model.getViewModel().getToggleFilters().getSelectedFilters().contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES)); + model.getViewModel().getToggleFilters().getSelectedFilters().remove(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES); + ACondition.waitFor("Cloud CLI Button to appear", 500, () -> harness.assertButton(model, ENABLE_CLOUD_CLI_BUTTON)); + + ButtonModel button = harness.assertButton(model, ENABLE_CLOUD_CLI_BUTTON); + button.perform(ui()); + assertNotNull(harness.getButton(model, ENABLE_CLOUD_CLI_BUTTON)); + verify(ui()).errorPopup(contains("Auto installation of Spring Cloud CLI not possible"), contains("Couldn't determine a compatible Spring Cloud CLI version")); + verifyNoMoreInteractions(ui()); + } + + private IBootInstall performCustomCliInstall(URL customZipUrl) throws Exception { + File workdir = StsTestUtil.createTempDirectory(); + File customZip = new File(workdir, "cli.zip"); + File installLocation = new File(workdir, "cli"); + FileUtils.copyURLToFile(customZipUrl, customZip); + ZipFileUtil.unzip(customZip, installLocation, new NullProgressMonitor()); + installLocation = installLocation.listFiles()[0]; + + BootInstallManager bootInstallMgr = context.getBootInstallManager(); + IBootInstall customInstall = bootInstallMgr.newInstall(installLocation.toURI().toString(), "custom-"+installLocation.getName()); + bootInstallMgr.setDefaultInstall(customInstall); //Note: implictly adds the default install if its not there yet, + FileUtils.deleteQuietly(customZip); + return customInstall; + } + + @Test public void testInvalidCloudCliInstall() throws Exception { + String error = "com.google.common.util.concurrent.UncheckedExecutionException: java.lang.NullPointerException\n" + + " at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2199)\n" + + " at com.google.common.cache.LocalCache.get(LocalCache.java:3932)\n" + + " at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:3936)"; + model.getViewModel().getToggleFilters().getSelectedFilters().remove(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES); + + BootCliCommand mockCommand = mock(BootCliCommand.class); + doReturn(error).when(mockCommand).getOutput(); + doReturn(0).when(mockCommand).execute(CloudCliInstall.LIST_SERVICES_COMMAND); + + IBootInstall defaultInstall = spy(context.getBootInstallManager().getDefaultInstall()); + CloudCliInstall mockCloudCliInstall = spy(new CloudCliInstall(defaultInstall)); + doReturn(new Version(1, 5, 1, null)).when(mockCloudCliInstall).getVersion(); + doReturn(mockCommand).when(mockCloudCliInstall).createCommand(); + doReturn(mockCloudCliInstall).when(defaultInstall).getExtension(CloudCliInstall.class); + + assertEquals(0, mockCloudCliInstall.getCloudServices().length); + + context.getBootInstallManager().setDefaultInstall(defaultInstall); + + assertServiceElements(); + } + + /** + * Test that newly created spring boot project gets added to the model. + */ + @Test public void testNewSpringBootProject() throws Exception { + +// assertWorkspaceProjects(/*none*/); + assertNonServiceElements(/*none*/); + + String projectName = "testProject"; + createBootProject(projectName); + new ACondition("Model update", MODEL_UPDATE_TIMEOUT) { + public boolean test() throws Exception { + assertNonServiceElements("testProject"); + return true; + } + }; + + BootDashElement projectEl = getElement("testProject"); + assertTrue(projectEl.getCurrentChildren().isEmpty()); + } + + /** + * Test that project with multiple associated launch configs has + * a child for each config. + */ + @Test + public void testSpringBootProjectChildren() throws Exception { + +// assertWorkspaceProjects(/*none*/); + assertNonServiceElements(/*none*/); + + String projectName = "testProject"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + new ACondition("Model update", MODEL_UPDATE_TIMEOUT) { + public boolean test() throws Exception { + assertNonServiceElements("testProject"); + return true; + } + }; + + BootDashElement projectEl = getElement("testProject"); + assertTrue(projectEl.getCurrentChildren().isEmpty()); + + ILaunchConfiguration conf1 = BootLaunchConfigurationDelegate.createConf(javaProject); + ILaunchConfiguration conf2 = BootLaunchConfigurationDelegate.createConf(javaProject); + assertFalse(conf1.equals(conf2)); + + ACondition.waitFor("Children to appear", 3000, () -> { + assertEquals(2, projectEl.getCurrentChildren().size()); + }); + + conf1.delete(); + + ACondition.waitFor("Child to disappear", 3000, () -> { + assertEquals(1, projectEl.getCurrentChildren().size()); + }); + } + + + /** + * Test that when a launch config is marked as 'hidden' it is not part of the model. + */ + @Test + public void testSpringBootProjectHiddenChildren() throws Exception { + assertNonServiceElements(/*none*/); + + String projectName = "testProject"; + IProject project = createBootProject(projectName); + IJavaProject javaProject = JavaCore.create(project); + new ACondition("Model update", MODEL_UPDATE_TIMEOUT) { + public boolean test() throws Exception { + assertNonServiceElements("testProject"); + return true; + } + }; + + final BootDashElement projectEl = getElement("testProject"); + assertTrue(projectEl.getCurrentChildren().isEmpty()); + + final ILaunchConfiguration[] conf = new ILaunchConfiguration[3]; + final BootDashElement[] el = new BootDashElement[conf.length]; + for (int i = 0; i < conf.length; i++) { + conf[i] = BootLaunchConfigurationDelegate.createConf(javaProject); + el[i] = harness.getElementFor(conf[i]); + } + + new ACondition("Wait for children", MODEL_UPDATE_TIMEOUT) { + public boolean test() throws Exception { + assertEquals(ImmutableSet.copyOf(el), projectEl.getCurrentChildren()); + return true; + } + }; + + hide(conf[2]); + new ACondition("Wait for child to disapear", MODEL_UPDATE_TIMEOUT) { + public boolean test() throws Exception { + assertEquals(ImmutableSet.of(el[0], el[1]), projectEl.getCurrentChildren()); + return true; + } + }; + + hide(conf[1]); + new ACondition("Wait for another child to disapear", MODEL_UPDATE_TIMEOUT) { + public boolean test() throws Exception { + //since there's just one conf left it is not shown as a child + assertEquals(ImmutableSet.of(el[0]), projectEl.getCurrentChildren()); + return true; + } + }; + + hide(conf[0]); + new ACondition("Wait for last child to disapear", MODEL_UPDATE_TIMEOUT) { + public boolean test() throws Exception { + //since there's just one conf left it is not shown as a child + assertEquals(ImmutableSet.of(), projectEl.getCurrentChildren()); + return true; + } + }; + } + + private void hide(ILaunchConfiguration conf) throws Exception { + ILaunchConfigurationWorkingCopy wc = conf.getWorkingCopy(); + BootLaunchConfigurationDelegate.setHiddenFromBootDash(wc, true); + wc.doSave(); + } + + private IProject createBootProject(String projectName, WizardConfigurer... extraConfs) throws Exception { + return projects.createBootWebProject(projectName, extraConfs); + } + + /** + * Test that when project is deleted from workspace it also deleted from the model + */ + @Test public void testDeleteProject() throws Exception { + String projectName = "testProject"; + IProject project = createBootProject(projectName); + + waitNonServiceElements("testProject"); + project.delete(/*delete content*/true, /*force*/true, /*progress*/null); + waitNonServiceElements(/*none*/); + } + + /** + * Test that when closed/opened it is removed/added to the model + */ + @Test public void testCloseAndOpenProject() throws Exception { + String projectName = "testProject"; + IProject project = createBootProject(projectName); + + waitNonServiceElements("testProject"); + + project.close(null); + waitNonServiceElements(); + + project.open(null); + waitNonServiceElements("testProject"); + } + + + /** + * Test that deleting a running launch conf works properly: + * 1) orphaned launch is terminated + * 2) BootProjectDashElement runstate is updated. + */ + @Test public void testDeleteRunningLaunchConfig() throws Exception { + doTestDeleteRunningLaunchConf(RunState.RUNNING); + } + + /** + * Test that deleting a running launch conf, (in debug mode) works properly: + * 1) orphaned launch is terminated + * 2) BootProjectDashElement runstate is updated. + */ + @Test public void testDeleteDebuggingLaunchConfig() throws Exception { + doTestDeleteRunningLaunchConf(RunState.DEBUGGING); + } + + private void doTestDeleteRunningLaunchConf(RunState runState) throws Exception, CoreException { + String projectName = "testProject"; + createBootProject(projectName); + waitNonServiceElements(projectName); + + BootProjectDashElement element = getElement(projectName); + element.openConfig(ui()); //Ensure that at least one launch config exists. + verify(ui()).openLaunchConfigurationDialogOnGroup(any(ILaunchConfiguration.class), any(String.class)); + verifyNoMoreInteractions(ui()); + ACondition.waitFor("child", 3000, () -> { + assertNotNull(getSingleValue(element.getCurrentChildren())); + }); + LaunchConfDashElement launchConfElement = (LaunchConfDashElement) getSingleValue(element.getCurrentChildren()); + ILaunchConfiguration launchConf = getSingleValue(launchConfElement.getLaunchConfigs()); + + element.restart(runState, null); + waitForState(element, runState); + waitForState(launchConfElement, runState); + + ILaunch launch = getSingleValue(launchConfElement.getLaunches()); + assertFalse(launch.isTerminated()); + + launchConf.delete(); + + ACondition.waitFor("Expectations after launchConf deleted", 2000, () -> { + assertTrue("launch terminated", launch.isTerminated()); + assertEquals(ImmutableSet.of(), element.getChildren().getValues()); + assertEquals(RunState.INACTIVE, element.getRunState()); + }); + } + + + private UserInteractions ui() { + return context.injections.getBean(AllUserInteractions.class); + } + + /** + * Test that element state listener for launch conf element is notified when it is + * launched via its project. + */ + @Test public void testLaunchConfRunStateChanges() throws Exception { + doTestLaunchConfRunStateChanges(RunState.RUNNING); + } + + /** + * Test that element state listener for launch conf element is notified when it is + * launched via its project. + */ + @Test public void testLaunchConfDebugStateChanges() throws Exception { + doTestLaunchConfRunStateChanges(RunState.DEBUGGING); + } + + protected void doTestLaunchConfRunStateChanges(RunState runState) throws Exception { + String projectName = "testProject"; + createBootProject(projectName); + waitNonServiceElements(projectName); + + BootProjectDashElement element = getElement(projectName); + element.openConfig(ui()); //Ensure that at least one launch config exists. + verify(ui()).openLaunchConfigurationDialogOnGroup(any(ILaunchConfiguration.class), any(String.class)); + verifyNoMoreInteractions(ui()); + ACondition.waitFor("child", 3000, () -> { + assertNotNull(getSingleValue(element.getCurrentChildren())); + }); + BootDashElement childElement = getSingleValue(element.getCurrentChildren()); + + ElementStateListener listener = mock(ElementStateListener.class); + model.addElementStateListener(listener); +// System.out.println("Element state listener ADDED"); +// model.addElementStateListener(new ElementStateListener() { +// public void stateChanged(BootDashElement e) { +// System.out.println("Changed: "+e); +// } +// }); + + element.restart(runState, null); + waitForState(element, runState); + waitForState(childElement, runState); + + ElementStateListener oldListener = listener; + + ACondition.waitFor("listener calls", 1000, () -> { + verify(oldListener, times(4)).stateChanged(element); + verify(oldListener, times(4)).stateChanged(childElement); + }); + model.removeElementStateListener(oldListener); + +// System.out.println("Element state listener REMOVED"); + + listener = mock(ElementStateListener.class); + model.addElementStateListener(listener); + + element.stop(); + waitForState(element, RunState.INACTIVE); + waitForState(childElement, RunState.INACTIVE); + + //4 changes: INACTIVE -> STARTING, STARTING -> RUNNING, livePort(set), actualInstances + verify(oldListener, times(4)).stateChanged(element); + verify(oldListener, times(4)).stateChanged(childElement); + //3 changes: RUNNING -> INACTIVE, liveport(unset), actualInstances + verify(listener, times(3)).stateChanged(element); + verify(listener, times(3)).stateChanged(childElement); + } + + + private T getSingleValue(ImmutableSet values) { + assertEquals("Unexpected number of values in "+values, 1, values.size()); + for (T e : values) { + return e; + } + throw new IllegalStateException("This code should be unreachable"); + } + + @Test @Ignore /* Ignored because unstable in CI builds, and its of diminishing importance */ + public void startLifeCycleDisabledApp() throws Exception { + String projectName = "some-app"; + IProject project = createBootProject(projectName, bootVersionAtLeast("1.3")); + + BootProjectDashElement element = getElement(projectName); + element.openConfig(ui()); //Ensure that at least one launch config exists. + verify(ui()).openLaunchConfigurationDialogOnGroup(any(ILaunchConfiguration.class), any(String.class)); + verifyNoMoreInteractions(ui()); + + //Disable lifecycle mgmt on config + ACondition.waitFor("child", 3000, () -> { + assertNotNull(getSingleValue(element.getCurrentChildren())); + }); + LaunchConfDashElement childElement = (LaunchConfDashElement) getSingleValue(element.getCurrentChildren()); + ILaunchConfigurationWorkingCopy wc = childElement.getActiveConfig().getWorkingCopy(); + assertNotNull(BootLaunchConfigurationDelegate.getMainType(wc)); + BootLaunchConfigurationDelegate.setEnableLifeCycle(wc, false); + wc.doSave(); + + doStartBootAppWithoutLifeCycleTest(element, RunState.RUNNING); + doStartBootAppWithoutLifeCycleTest(element, RunState.DEBUGGING); + } + + @Test @Ignore /* Ignored because unstable in CI builds, and its of diminishing importance */ + public void startOldBootApp() throws Exception { + String projectName = "boot12"; + createPredefinedMavenProject(projectName); + BootProjectDashElement element = getElement(projectName); + doStartBootAppWithoutLifeCycleTest(element, RunState.RUNNING); + doStartBootAppWithoutLifeCycleTest(element, RunState.DEBUGGING); + } + + private void doStartBootAppWithoutLifeCycleTest(BootProjectDashElement app, RunState runOrDebug) throws Exception { + try { + waitForState(app, RunState.INACTIVE); + app.restart(runOrDebug, ui()); + Thread.sleep(2000); //Give app a little bit of time to get going. + waitForState(app, runOrDebug); + } finally { + ACondition.waitFor("stop hammering", 20000, () -> { + app.stop(false); + assertEquals(RunState.INACTIVE, app.getRunState()); + }); + } + } + + private IProject createPredefinedMavenProject(String projectName) throws Exception { + return BootProjectTestHarness.createPredefinedMavenProject(projectName, "org.springframework.ide.eclipse.boot.dash.test"); + } + + /** + * Test that element state listener is notified when a project is launched and terminated. + */ + @Test public void testRunStateChanges() throws Exception { + doTestRunStateChanges(RunState.RUNNING); + } + + /** + * Test that element state listener is notified when a project is launched in Debug mode and terminated. + */ + @Test public void testDebugStateChanges() throws Exception { + doTestRunStateChanges(RunState.DEBUGGING); + } + + protected void doTestRunStateChanges(RunState runState) throws Exception { + String projectName = "testProject"; + createBootProject(projectName); + waitNonServiceElements(projectName); + + ElementStateListener listener = mock(ElementStateListener.class); + model.addElementStateListener(listener); + //System.out.println("Element state listener ADDED"); + BootDashElement element = getElement(projectName); + element.restart(runState, null); + waitForState(element, runState); + + ElementStateListener oldListener = listener; + ACondition.waitFor("listener calls", 1000, () -> + verify(oldListener, times(4)).stateChanged(element) + ); + model.removeElementStateListener(oldListener); + //System.out.println("Element state listener REMOVED"); + + listener = mock(ElementStateListener.class); + model.addElementStateListener(listener); + + element.stop(); + waitForState(element, RunState.INACTIVE); + + //4 changes: INACTIVE -> STARTING, STARTING -> RUNNING, livePort(set), actualInstances++ + verify(oldListener, times(4)).stateChanged(element); + //3 changes: RUNNING -> INACTIVE, liveport(unset), actualInstances-- + ElementStateListener newListener = listener; + ACondition.waitFor("listener calls", 1000, () -> + verify(newListener, times(3)).stateChanged(element) + ); + } + + @Test public void projectElementDisposedWhenProjectClosed() throws Exception { + String projectName = "testProject"; + IProject project = createBootProject(projectName); + waitNonServiceElements(projectName); + + BootProjectDashElement projectElement = getElement(projectName); + LiveVariable disposed = new LiveVariable<>(false); + projectElement.onDispose((d) -> disposed.setValue(true)); + + project.close(new NullProgressMonitor()); + + ACondition.waitFor("Element disposed", 100, () -> { + assertTrue(disposed.getValue()); + }); + } + + @Test public void testRestartRunningProcessTest() throws Exception { + String projectName = "testProject"; + createBootProject(projectName); + waitNonServiceElements(projectName); + + final RunState[] RUN_STATES = { + RunState.RUNNING, + RunState.DEBUGGING + }; + + for (RunState fromState : RUN_STATES) { + for (RunState toState : RUN_STATES) { + doRestartTest(projectName, fromState, toState); + } + } + } + + @Test public void testDevtoolsPortRefreshedOnRestart() throws Exception { + //Test that the local bootdash element 'liveport' is updated when boot devtools + // does an in-place restart of the app, changing the port that it runs on. + String projectName = "some-project-with-devtools"; + createBootProject(projectName, bootVersionAtLeast("1.3.0"), //1.3.0 required for lifecycle & devtools support + withStarters("devtools") + ); + + final BootProjectDashElement project = getElement(projectName); + try { + waitForState(project, RunState.INACTIVE); + System.out.println("Starting "+project); + project.restart(RunState.RUNNING, ui()); + waitForState(project, RunState.RUNNING); + + BootDashElement launch = CollectionUtils.getSingle(project.getChildren().getValues()); + + int defaultPort = 8080; + int changedPort = 8765; + + waitForPort(project, defaultPort); + + System.out.println("Changing port in application.properties to "+changedPort); + IFile props = project.getProject().getFile(new Path("src/main/resources/application.properties")); + setContents(props, "server.port="+changedPort); + System.out.println("Rebuilding project..."); + StsTestUtil.assertNoErrors(project.getProject()); + System.out.println("Rebuilding project... DONE"); + //builds the project... should trigger devtools to 'refresh'. + + waitForPort(project, changedPort); + waitForPort(launch, changedPort); + + //Now try that this also works in debug mode... + System.out.println("Restart project in DEBUG mode..."); + project.restart(RunState.DEBUGGING, ui()); + waitForState(project, RunState.DEBUGGING); + + waitForPort(project, changedPort); + waitForPort(launch, changedPort); + + System.out.println("Changing port in application.properties to "+defaultPort); + setContents(props, "server.port="+defaultPort); + System.out.println("Rebuilding project..."); + StsTestUtil.assertNoErrors(project.getProject()); + System.out.println("Rebuilding project... DONE"); + //builds the project... should trigger devtools to 'refresh'. + waitForPort(project, defaultPort); + waitForPort(launch, defaultPort); + + } finally { + System.out.println("Cleanup: stop "+project); + project.stop(); + waitForState(project, RunState.INACTIVE); + } + } + + protected void waitForPort(final BootDashElement element, final int expectedPort) throws Exception { + new ACondition("Wait for port on "+element.getName()+" to change to "+expectedPort, 5000) { //Devtools should restart really fast + @Override + public boolean test() throws Exception { + assertEquals(ImmutableSet.of(expectedPort), element.getLivePorts()); + assertEquals(expectedPort, element.getLivePort()); + return true; + } + }; + } + + @Test public void testStartingStateObservable() throws Exception { + //Test that, for boot project that supports it, the 'starting' state + // is observable in the model. + String projectName = "some-project"; + createBootProject(projectName, + bootVersionAtLeast("1.3.0") //1.3.0 required for lifecycle support + ); + BootProjectDashElement element = getElement(projectName); + try { + waitForState(element, RunState.INACTIVE); + + RecordingElementChangedListener recordedChanges1 = new RecordingElementChangedListener(); + model.addElementStateListener(recordedChanges1); + + element.restart(RunState.RUNNING, ui()); + waitForState(element, RunState.RUNNING); + + List states1 = recordedChanges1.getRecordedRunStates(); + assertEquals(4, states1.size()); + assertEquals(RunState.STARTING, states1.get(0)); + assertEquals(RunState.RUNNING, states1.get(1)); + + model.removeElementStateListener(recordedChanges1); + + RecordingElementChangedListener recordedChanges2 = new RecordingElementChangedListener(); + model.addElementStateListener(recordedChanges2); + + element.restart(RunState.DEBUGGING, ui()); + waitForState(element, RunState.DEBUGGING); + + List states2 = recordedChanges2.getRecordedRunStates(); + assertEquals(7, states2.size()); + assertEquals(RunState.STARTING, states2.get(3)); + assertEquals(RunState.DEBUGGING, states2.get(4)); + + model.removeElementStateListener(recordedChanges2); + + } finally { + element.stop(); + waitForState(element, RunState.INACTIVE); + } + } + + private void doRestartTest(String projectName, RunState fromState, RunState toState) throws Exception { + BootProjectDashElement element = getElement(projectName); + try { + element.restart(fromState, ui()); + waitForState(element, fromState); + + final ILaunch launch = getActiveLaunch(element); + + element.restart(toState, ui()); + + //Watch out for race conditions... we can't really reliably observe the + // 'terminated' state of the element, as we don't know how long it will + // last and the 'restart' operation may happen concurrently with the testing + // thread. Therefore we observe the terminated state of the actual launch. + // Restarting the project will/should terminate the old launch and then + // create a new launch. + + new ACondition("Wait for launch termination", RUN_STATE_CHANGE_TIMEOUT) { + public boolean test() throws Exception { + return launch.isTerminated(); + } + }; + + waitForState(element, toState); + } finally { + element.stop(); + waitForState(element, RunState.INACTIVE); + } + } + + @Test public void livePortSummaryAndInstanceCounts() throws Exception { + String projectName = "some-project"; + createBootProject(projectName, bootVersionAtLeast("1.3.0")); //1.3.0 required for lifecycle support. + final BootProjectDashElement project = getElement(projectName); + try { + assertEquals(RunState.INACTIVE, project.getRunState()); + assertTrue(project.getLivePorts().isEmpty()); // live port is 'unknown' if app is not running + assertInstances("0/1", project); + assertInstancesLabel("", project); //label hidden for ?/1 case + + IType mainType = MainTypeFinder.guessMainTypes(project.getJavaProject(), new NullProgressMonitor())[0]; + + final int port1 = portFinder.findUniqueFreePort(); + ILaunchConfiguration config1 = BootLaunchConfigurationDelegate.createConf(mainType); + setPort(config1, port1); + + final int port2 = portFinder.findUniqueFreePort(); + ILaunchConfiguration config2 = BootLaunchConfigurationDelegate.createConf(mainType); + setPort(config2, port2); + + final BootDashElement el1 = harness.getElementFor(config1); + final BootDashElement el2 = harness.getElementFor(config2); + + assertInstances("0/1", el1); + assertInstancesLabel("", el1); // hidden label for ?/1 case + assertInstances("0/1", el2); + assertInstancesLabel("", el2); // hidden label for ?/1 case + + el1.restart(RunState.RUNNING, ui()); + el2.restart(RunState.RUNNING, ui()); + + waitForState(el1, RunState.RUNNING); + waitForState(el2, RunState.RUNNING); + + new ACondition("check port summary", MODEL_UPDATE_TIMEOUT) { + public boolean test() throws Exception { + assertInstances("2/2", project); + assertInstancesLabel("2/2", project); + assertInstances("1/1", el1); + assertInstancesLabel("", el1); // hidden label for ?/1 case + assertInstances("1/1", el2); + assertInstancesLabel("", el2); // hidden label for ?/1 case + + assertEquals(port1, el1.getLivePort()); + assertEquals(port2, el2.getLivePort()); + + assertEquals(ImmutableSet.of(port1, port2), project.getLivePorts()); + return true; + } + }; + + el1.stop(); + new ACondition("check port summary", MODEL_UPDATE_TIMEOUT) { + public boolean test() throws Exception { + assertEquals(ImmutableSet.of(port2), project.getLivePorts()); + assertEquals(1, project.getActualInstances()); + assertEquals(2, project.getDesiredInstances()); + + assertInstances("1/2", project); + assertInstancesLabel("1/2", project); + assertInstances("0/1", el1); + assertInstancesLabel("", el1); // hidden label for ?/1 case + assertInstances("1/1", el2); + assertInstancesLabel("", el2); // hidden label for ?/1 case + + return true; + } + }; + + + } finally { + project.stopSync(); + } + } + + private void assertInstances(String expect, BootDashElement e) { + assertEquals(expect, e.getActualInstances()+"/"+e.getDesiredInstances()); + } + + private void assertInstancesLabel(String expect, BootDashElement e) { + harness.assertInstancesLabel(expect, e); + } + + @Test public void livePort() throws Exception { + String projectName = "some-project"; + createBootProject(projectName, bootVersionAtLeast("1.3.0")); //1.3.0 required for lifecycle support. + + final BootProjectDashElement element = getElement(projectName); + assertEquals(RunState.INACTIVE, element.getRunState()); + assertEquals(-1, element.getLivePort()); // live port is 'unknown' if app is not running + try { + waitForState(element, RunState.INACTIVE); + + element.restart(RunState.RUNNING, ui()); + waitForState(element, RunState.RUNNING); + + new ACondition(4000) { + public boolean test() throws Exception { + assertEquals(8080, element.getLivePort()); + return true; + } + }; + + //Change port in launch conf and restart + ILaunchConfiguration conf = element.getActiveConfig(); + ILaunchConfigurationWorkingCopy wc = conf.getWorkingCopy(); + BootLaunchConfigurationDelegate.setRawApplicationProperties(wc, "server.port=6789"); + wc.doSave(); + final BootDashElement childElement = getSingleValue(element.getCurrentChildren()); + + new ACondition(4000) { + public boolean test() throws Exception { + assertEquals(8080, element.getLivePort()); // port still the same until we restart + assertEquals(8080, childElement.getLivePort()); + return true; + } + }; + + element.restart(RunState.RUNNING, ui()); + waitForState(element, RunState.RUNNING); + new ACondition(4000) { + public boolean test() throws Exception { + assertEquals(6789, element.getLivePort()); + assertEquals(6789, childElement.getLivePort()); + return true; + } + }; + + } finally { + element.stop(); + waitForState(element, RunState.INACTIVE); + } + } + + @Test public void testRequestMappings_2_x() throws Exception { + String projectName = "actuated-project"; + IProject project = createBootProject(projectName, + bootVersionAtLeast("2.0"), //Ensure we test with 2.0, actuator endpoints are changed in 2.0! + withStarters("web", "actuator"), //required to actually *have* an actuator + setPackage("com.example.demo") + ); + createFile(project, "src/main/java/com/example/demo/HelloController.java", + "package com.example.demo;\n" + + "\n" + + "import org.springframework.web.bind.annotation.RequestMapping;\n" + + "import org.springframework.web.bind.annotation.RestController;\n" + + "\n" + + "@RestController\n" + + "public class HelloController {\n" + + "\n" + + " @RequestMapping(\"/hello\")\n" + + " public String hello() {\n" + + " return \"Hello, World!\";\n" + + " }\n" + + "\n" + + "}\n" + ); + StsTestUtil.assertNoErrors(project); + final BootDashElement element = getElement(projectName); + try { + waitForState(element, RunState.INACTIVE); + assertNull(element.getLiveRequestMappings().orElse(null)); // unknown since can only be determined when app is running + + element.restart(RunState.RUNNING, ui()); + waitForState(element, RunState.RUNNING); + new ACondition("Wait for request mappings", MODEL_UPDATE_TIMEOUT) { + public boolean test() throws Exception { + List mappings = element.getLiveRequestMappings().orElse(null); + assertNotNull(mappings); //Why is the test sometimes failing here? + assertTrue(!mappings.isEmpty()); //Even though this is an 'empty' app should have some mappings, + // for example an 'error' page. + return true; + } + }; + + List mappings = element.getLiveRequestMappings().orElse(null); + System.out.println(">>> Found RequestMappings"); + for (RequestMapping m : mappings) { + System.out.println(m.getPath()); + assertNotNull(m.getPath()); + } + System.out.println("<<< Found RequestMappings"); + + RequestMapping rm; + //Case 2 examples (path extracted from 'pseudo' json in the key) + rm = assertRequestMappingWithPath(mappings, "/hello"); //We defined it so should be there + assertEquals("com.example.demo.HelloController", rm.getFullyQualifiedClassName()); + assertEquals("hello", rm.getMethodName()); + assertEquals("com.example.demo.HelloController", rm.getType().getFullyQualifiedName()); + + IMethod method = rm.getMethod(); + assertEquals(rm.getType(), method.getDeclaringType()); + assertEquals("hello", method.getElementName()); + + assertTrue(rm.isUserDefined()); + + rm = assertRequestMappingWithPath(mappings, "/error"); //Even empty apps should have a 'error' mapping + assertFalse(rm.isUserDefined()); + + rm = assertRequestMappingWithPath(mappings, "/actuator/health"); +// assertNotNull(rm.getMethod()); +// assertNotNull(rm.getType()); + assertFalse(rm.isUserDefined()); + + rm = assertRequestMappingWithPath(mappings, "/actuator/info"); +// assertNotNull(rm.getMethod()); +// assertNotNull(rm.getType()); + assertFalse(rm.isUserDefined()); + + //Case 1 example (path represented directly in the json key). + rm = assertRequestMappingWithPath(mappings, "/actuator/health"); + assertFalse(rm.isUserDefined()); + + } finally { + element.stop(); + waitForState(element, RunState.INACTIVE); + } + } + + @Test public void testRequestMappingsOnlyJmxEnabled() throws Exception { + do_testRequestMappingsOnlyJmxEnabled(RunState.RUNNING); + } + + @Test public void testRequestMappingsOnlyJmxEnabled_DEBUG() throws Exception { + do_testRequestMappingsOnlyJmxEnabled(RunState.DEBUGGING); + } + + private void do_testRequestMappingsOnlyJmxEnabled(RunState runMode) throws Exception, CoreException { + String projectName = "actuated-project"; + IProject project = createBootProject(projectName, + bootVersionAtLeast("1.3.0"), //required for us to be able to determine the actuator port + withStarters("web", "actuator"), //required to actually *have* an actuator + setPackage("com.example.demo") + ); + createFile(project, "src/main/java/com/example/demo/HelloController.java", + "package com.example.demo;\n" + + "\n" + + "import org.springframework.web.bind.annotation.RequestMapping;\n" + + "import org.springframework.web.bind.annotation.RestController;\n" + + "\n" + + "@RestController\n" + + "public class HelloController {\n" + + "\n" + + " @RequestMapping(\"/hello\")\n" + + " public String hello() {\n" + + " return \"Hello, World!\";\n" + + " }\n" + + "\n" + + "}\n" + ); + StsTestUtil.assertNoErrors(project); + final BootProjectDashElement element = getElement(projectName); + ILaunchConfigurationWorkingCopy wc = element.createLaunchConfigForEditing().getWorkingCopy(); + BootLaunchConfigurationDelegate.setEnableLifeCycle(wc, false); + BootLaunchConfigurationDelegate.setEnableLiveBeanSupport(wc,false); + wc.doSave(); + try { + waitForState(element, RunState.INACTIVE); + assertNull(element.getLiveRequestMappings().orElse(null)); // unknown since can only be determined when app is running + + element.restart(runMode, ui()); + waitForState(element, runMode); + ACondition.waitFor("Wait for request mappings", MODEL_UPDATE_TIMEOUT, () -> { + List mappings = element.getLiveRequestMappings().orElse(null); + assertNotNull(mappings); + assertTrue(!mappings.isEmpty()); //Even though this is an 'empty' app should have some mappings, + // for example an 'error' page. + }); + + List mappings = element.getLiveRequestMappings().orElse(null); + System.out.println(">>> Found RequestMappings"); + for (RequestMapping m : mappings) { + System.out.println(m.getPath()); + assertNotNull(m.getPath()); + } + System.out.println("<<< Found RequestMappings"); + + RequestMapping rm = assertRequestMappingWithPath(mappings, "/hello"); //We defined it so should be there + assertEquals("com.example.demo.HelloController", rm.getFullyQualifiedClassName()); + assertEquals("hello", rm.getMethodName()); + assertEquals("com.example.demo.HelloController", rm.getType().getFullyQualifiedName()); + + IMethod method = rm.getMethod(); + assertEquals(rm.getType(), method.getDeclaringType()); + assertEquals("hello", method.getElementName()); + + assertTrue(rm.isUserDefined()); + + rm = assertRequestMappingWithPath(mappings, "/error"); //Even empty apps should have a 'error' mapping + assertFalse(rm.isUserDefined()); + + } finally { + element.stop(); + waitForState(element, RunState.INACTIVE); + } + } + + + @Test public void testDefaultRequestMapping() throws Exception { + String projectName = "sdfsd-project"; + createBootProject(projectName); + BootDashElement element = getElement(projectName); + + assertNull(element.getDefaultRequestMappingPath()); + element.setDefaultRequestMappingPath("something"); + assertProjectProperty(element.getProject(), "default.request-mapping.path", "something"); + + assertEquals("something", element.getDefaultRequestMappingPath()); + } + + @Test public void testDevtoolsTextDecorationOnLocalElements() throws Exception { + final String projectName = "project-hahaha"; + IProject project = createBootProject(projectName, withStarters("web", "actuator", "devtools")); + final BootDashElement element = getElement(projectName); + assertTrue(element.projectHasDevtoolsDependency()); + harness.assertLabelContains("[devtools]", element); + + //Also check that we do not add 'devtools' label to launch configs. + ILaunchConfiguration conf = BootLaunchConfigurationDelegate.createConf(project); + String confName = conf.getName(); + + assertEquals(confName, harness.getLabel(harness.getElementFor(conf))); + + //Try and see that if we remove the devtools dependency from the project then the label updates. + StsTestUtil.setAutoBuilding(true); // so that autobuild causes classpath update as would + // happen in a 'real' workspace when pom is changed. + IMavenCoordinates devtools = removeDevtools(project); + new ACondition("Wait for devtools to disapear", MAVEN_BUILD_TIMEOUT) { + public boolean test() throws Exception { + assertFalse(element.projectHasDevtoolsDependency()); + assertEquals(projectName, harness.getLabel(element)); + return true; + } + }; + + springBootCore.project(project).addMavenDependency(devtools, true); + new ACondition("Wait for devtools to re-apear", MAVEN_BUILD_TIMEOUT) { + public boolean test() throws Exception { + assertFalse(element.projectHasDevtoolsDependency()); + assertEquals(projectName, harness.getLabel(element)); + return true; + } + }; + + } + + /************************************************************************************** + * TAGS Tests START + *************************************************************************************/ + + private void testSettingTags(String[] tagsToSet, String[] expectedTags) throws Exception { + String projectName = "alex-project"; + createBootProject(projectName); + BootDashElement element = getElement(projectName); + IProject project = element.getProject(); + + if (tagsToSet==null || tagsToSet.length==0) { + element.setTags(new LinkedHashSet<>(Arrays.asList("foo", "bar"))); + assertFalse(element.getTags().isEmpty()); + } else { + assertArrayEquals(new String[]{}, element.getTags().toArray(new String[0])); + } + + element.setTags(linkedHashSet(tagsToSet)); + waitForJobsToComplete(); + assertArrayEquals(expectedTags, element.getTags().toArray(new String[0])); + + // Reopen the project to load tags from the resource + project.close(null); + project.open(null); + element = getElement(projectName); + assertArrayEquals(expectedTags, element.getTags().toArray(new String[0])); + } + + private LinkedHashSet linkedHashSet(String[] tagsToSet) { + if (tagsToSet!=null) { + return new LinkedHashSet<>(Arrays.asList(tagsToSet)); + } + return null; + } + + @Test + public void setUniqueTagsForProject() throws Exception { + testSettingTags(new String[] {"xd", "spring"}, new String[] {"xd", "spring"}); + } + + @Test + public void setDuplicateTagsForProject() throws Exception { + testSettingTags(new String[] {"xd", "spring", "xd", "spring", "spring"}, new String[] {"xd", "spring"}); + } + + @Test + public void setTagsWithWhiteSpaceCharsForProject() throws Exception { + testSettingTags(new String[] {"#xd", "\tspring", "xd ko ko", "spring!!-@", "@@@ - spring"}, new String[] {"#xd", "\tspring", "xd ko ko", "spring!!-@", "@@@ - spring"}); + } + + @Test + public void setNoTags() throws Exception { + testSettingTags(new String[0], new String[0]); + } + + @Test + public void setNullTags() throws Exception { + testSettingTags(null, new String[0]); + } + + private class BdeInfo { + String name; + String[] tags; + String workingSet; + + BdeInfo(String name, String[] tags, String workingSet) { + this.name = name; + this.tags = tags; + this.workingSet = workingSet; + } + } + + /************************************************************************************** + * TAGS Tests END + *************************************************************************************/ + + /************************************************************************************** + * BDEs Filtering Tests START + *************************************************************************************/ + + private void testBdeFiltering(BdeInfo[] bdeInfo, String filterText, String[] expectedBdes) throws Exception { + Map> wsMap = new HashMap<>(); + List bdes = new ArrayList<>(bdeInfo.length); + for (BdeInfo info : bdeInfo) { + createBootProject(info.name); + BootDashElement element = getElement(info.name); + IProject project = element.getProject(); + if (info.tags != null && info.tags.length > 0) { + element.setTags(new LinkedHashSet<>(Arrays.asList(info.tags))); + } + if (info.workingSet != null && !info.workingSet.isEmpty() && project != null) { + List projects = wsMap.get(info.workingSet); + if (projects == null) { + projects = new ArrayList<>(); + wsMap.put(info.workingSet, projects); + } + projects.add(project); + } + bdes.add(element); + } + IWorkingSetManager wsManager = PlatformUI.getWorkbench().getWorkingSetManager(); + for (Map.Entry> entry : wsMap.entrySet()) { + IWorkingSet ws = wsManager.getWorkingSet(entry.getKey()); + if (ws == null) { + ws = wsManager.createWorkingSet(entry.getKey(), entry.getValue().toArray(new IProject[entry.getValue().size()])); + wsManager.addWorkingSet(ws); + } else { + ws.setElements(entry.getValue().toArray(new IProject[entry.getValue().size()])); + } + } + waitForJobsToComplete(); + + BootDashElementsFilterBoxModel filterModel = new BootDashElementsFilterBoxModel(); + filterModel.getText().setValue(filterText); + Filter filter = filterModel.getFilter().getValue(); + + List result = new ArrayList<>(); + for (BootDashElement bde : bdes) { + if (filter.accept(bde) && bde.getProject() != null) { + result.add(bde.getProject().getName()); + } + } + String[] actualBdes = result.toArray(new String[result.size()]); + assertArrayEquals(expectedBdes, actualBdes); + } + + @Test + public void testNoWorkingSetMatch_1() throws Exception { + testBdeFiltering(new BdeInfo[]{new BdeInfo("a", null, null), new BdeInfo("b", null, null)}, "x", new String[0]); + } + + @Test + public void testNoWorkingSetMatch_2() throws Exception { + testBdeFiltering(new BdeInfo[]{new BdeInfo("a", null, "xxx"), new BdeInfo("b", null, "xxx")}, "x,", new String[0]); + } + + @Test + public void testNoWorkingSetMatch_3() throws Exception { + testBdeFiltering( + new BdeInfo[]{ + new BdeInfo("b", null, "xxx"), + new BdeInfo("c", null, "xxx") + }, + "xxx, a", + new String[0] + ); + } + + @Test + public void testNoWorkingSetMatch_4() throws Exception { + testBdeFiltering( + new BdeInfo[]{ + new BdeInfo("a", null, "xxx"), + new BdeInfo("b", null, "xxx") + }, + "c, x", + new String[0] + ); + } + + @Test + public void testWorkingSetMatch_1() throws Exception { + testBdeFiltering(new BdeInfo[]{new BdeInfo("a", null, "x"), new BdeInfo("b", null, null), new BdeInfo("c", null, "x")}, "x", new String[]{"a", "c"}); + } + + @Test + public void testWorkingSetMatch_2() throws Exception { + testBdeFiltering(new BdeInfo[]{new BdeInfo("a", null, "xxx"), new BdeInfo("b", null, null), new BdeInfo("c", null, "xxxx")}, "xxx,", new String[]{"a"}); + } + + @Test + public void testWorkingSetMatch_3() throws Exception { + testBdeFiltering( + new BdeInfo[]{ + new BdeInfo("a", new String[]{"aaa", "bbb"}, "xxx"), + new BdeInfo("b", new String[]{"a", "c"}, "xxx") + }, + "xxx, a", + new String[]{"a", "b"}); + } + + @Test + public void testWorkingSetMatch_4() throws Exception { + testBdeFiltering( + new BdeInfo[]{ + new BdeInfo("z", + new String[]{"aaa", "bbb"}, + "xxx" + ), + new BdeInfo("b", + new String[]{"a", "c"}, + "xxx") + }, + "a, x", + new String[]{"b"} + ); + } + + /************************************************************************************** + * BDEs Filtering Tests END + *************************************************************************************/ + + ///////////////// harness code //////////////////////// + + private void assertProjectProperty(IProject project, String prop, String value) { + assertEquals(value, context.getProjectProperties().get(project, prop)); + } + + @Rule + public TestRule listenerLeakDetector = new ListenerLeakDetector(); + + @Rule + public LaunchCleanups launchCleanups = new LaunchCleanups(); + + private BootDashViewModelHarness harness; + + @Before + public void setup() throws Exception { + //As part of its normal operation, devtools will throw some uncaucht exceptions. + // We don't want our tests to be disrupted when running the process in debug mode... so disable + // suspending on such exceptions: + suspendOnUncaughtException(false); + + StsTestUtil.deleteAllProjects(); + this.context = new TestBootDashModelContext( + ResourcesPlugin.getWorkspace(), + DebugPlugin.getDefault().getLaunchManager() + ); + this.harness = new BootDashViewModelHarness(context.withTargetTypes(RunTargetTypes.LOCAL)); + this.model = harness.getRunTargetModel(RunTargetTypes.LOCAL); + this.projects = new BootProjectTestHarness(context.getWorkspace()); + StsTestUtil.setAutoBuilding(false); + + } + + public static void suspendOnUncaughtException(boolean enable) { + String suspendOption = "org.eclipse.jdt.debug.ui.javaDebug.SuspendOnUncaughtExceptions"; + IEclipsePreferences debugPrefs = InstanceScope.INSTANCE.getNode("org.eclipse.jdt.debug.ui"); + debugPrefs.putBoolean(suspendOption, enable); + } + + @After + public void tearDown() throws Exception { + /* + * Remove any working sets created by the tests (BDEs filtering tests create working sets) + */ + IWorkingSetManager wsManager = PlatformUI.getWorkbench().getWorkingSetManager(); + for (IWorkingSet ws : wsManager.getAllWorkingSets()) { + if (!ws.isAggregateWorkingSet()) { + wsManager.removeWorkingSet(ws); + } + } + + this.harness.dispose(); + } + + /** + * Returns the only active (i.e. not terminated launch for a project). If there is more + * than one active launch, or no active launch this returns null. + */ + public ILaunch getActiveLaunch(BootDashElement element) { + ImmutableSet ls = getBootLaunches(element); + ILaunch activeLaunch = null; + for (ILaunch l : ls) { + if (!l.isTerminated()) { + if (activeLaunch==null) { + activeLaunch = l; + } else { + //More than one active launch + return null; + } + } + } + return activeLaunch; + } + + private ImmutableSet getBootLaunches(BootDashElement element) { + if (element instanceof BootProjectDashElement) { + BootProjectDashElement project = (BootProjectDashElement) element; + return project.getLaunches(); + } + return ImmutableSet.of(); + } + + private static void waitForState(final BootDashElement element, final RunState state) throws Exception { + new ACondition("Wait for state "+state, RUN_STATE_CHANGE_TIMEOUT) { + @Override + public boolean test() throws Exception { + assertEquals(state, element.getRunState()); + return true; + } + }; + } + + private BootProjectDashElement getElement(String name) { + for (BootDashElement el : model.getElements().getValues()) { + if (name.equals(el.getName())) { + return (BootProjectDashElement) el; + } + } + return null; + } + + private void assertServiceElements(String... expectedElementNames) { + Set elements = model.getElements().getValue(); + Set names = new HashSet<>(); + for (BootDashElement e : elements) { + if (isService(e)) { + names.add(e.getName()); + } + } + assertElements(names, expectedElementNames); + } + + private void assertNonServiceElements(String... expectedElementNames) { + Set elements = model.getElements().getValue(); + Set names = new HashSet<>(); + for (BootDashElement e : elements) { + if (!isService(e)) { + names.add(e.getName()); + } + } + assertElements(names, expectedElementNames); + } + + private boolean isService(BootDashElement e) { + return e instanceof LocalCloudServiceDashElement + || e instanceof CloudServiceInstanceDashElement; + } + + public void waitNonServiceElements(final String... expectedElementNames) throws Exception { + new ACondition("Model update", MODEL_UPDATE_TIMEOUT) { + public boolean test() throws Exception { + assertNonServiceElements(expectedElementNames); + return true; + } + }; + } + + public static void waitForJobsToComplete() throws Exception { + new ACondition("Wait for Jobs", 3 * 60 * 1000) { + @Override + public boolean test() throws Exception { + assertJobManagerIdle(); + return true; + } + }; + } + + private void setPort(ILaunchConfiguration conf, int port) throws Exception { + ILaunchConfigurationWorkingCopy wc = conf.getWorkingCopy(); + assertTrue("Only supported on 'empty' configs", BootLaunchConfigurationDelegate.getApplicationProperties(wc).isEmpty()); + BootLaunchConfigurationDelegate.setRawApplicationProperties(wc, "server.port=" + port); + wc.doSave(); + } + + private IMavenCoordinates removeDevtools(IProject project) throws Exception { + ISpringBootProject bootProject = springBootCore.project(project); + MavenId devtoolsId = new MavenId( + BootPropertyTester.SPRING_BOOT_DEVTOOLS_GID, + BootPropertyTester.SPRING_BOOT_DEVTOOLS_AID + ); + + IMavenCoordinates devtools = null; + for (IMavenCoordinates dep : bootProject.getDependencies()) { + if (new MavenId(dep.getGroupId(), dep.getArtifactId()).equals(devtoolsId)) { + devtools = dep; + } + } + assertNotNull("Devtools dependency not found, so can't remove it", devtools); + bootProject.removeMavenDependency(devtoolsId); + return devtools; + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashTestBundleConstants.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashTestBundleConstants.java new file mode 100644 index 000000000..b9f8f1219 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashTestBundleConstants.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test; + +/** + */ +public class BootDashTestBundleConstants { + + public static final String BUNDLE_ID = "org.springframework.ide.eclipse.boot.dash.test"; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashViewModelHarness.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashViewModelHarness.java new file mode 100644 index 000000000..30e85aaaa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashViewModelHarness.java @@ -0,0 +1,333 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.assertContains; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.core.resources.IProject; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.graphics.Color; +import org.junit.Assert; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement; +import org.springframework.ide.eclipse.boot.dash.model.ButtonModel; +import org.springframework.ide.eclipse.boot.dash.model.LocalBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.RunTargets; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElement; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockMultiSelection; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +import com.google.common.collect.ImmutableSet; + +import junit.framework.AssertionFailedError; + +public class BootDashViewModelHarness { + + public TestBootDashModelContext context; + public BootDashViewModel model; + public final MockMultiSelection selection = new MockMultiSelection<>(BootDashElement.class); + public final LiveVariable sectionSelection = new LiveVariable<>(); + + /** + * This is private now. Use the Builder instead for a convenient way to create a harness with some targets in it. + */ + public BootDashViewModelHarness(TestBootDashModelContext context) { + this.context = context; + this.model = context.injections.getBean(BootDashViewModel.class); + } + + /** + * Dipose model and reinitialze it reusing the same stores (for testing functionality + * around persisting stuff) + */ + public void reload() throws Exception { + dispose(); + context = context.reload(); + this.model = context.injections.getBean(BootDashViewModel.class); + } + +// public static class MockContext implements BootDashModelContext { +// +// private IPropertyStore viewProperties = new InMemoryPropertyStore(); +// private IPropertyStore privateProperties = new InMemoryPropertyStore(); +// +// private IScopedPropertyStore projectProperties = new MockScopedPropertyStore<>(); +// private IScopedPropertyStore runtargetProperties = new MockScopedPropertyStore<>(); +// private SecuredCredentialsStore secureStore = new MockSecuredCredentialStore(); +// private File stateLocation; +// private LiveVariable bootProjectExclusion = new LiveVariable<>(BootPreferences.DEFAULT_BOOT_PROJECT_EXCLUDE); +// +// public MockContext() throws Exception { +// stateLocation = StsTestUtil.createTempDirectory(); +// } +// +// @Override +// public IWorkspace getWorkspace() { +// return ResourcesPlugin.getWorkspace(); +// } +// +// @Override +// public ILaunchManager getLaunchManager() { +// return DebugPlugin.getDefault().getLaunchManager(); +// } +// +// @Override +// public IPath getStateLocation() { +// return new Path(stateLocation.toString()); +// } +// +// @Override +// public IScopedPropertyStore getProjectProperties() { +// return projectProperties; +// } +// +// @Override +// public IScopedPropertyStore getRunTargetProperties() { +// return runtargetProperties; +// } +// +// @Override +// public SecuredCredentialsStore getSecuredCredentialsStore() { +// return secureStore; +// } +// +// @Override +// public void log(Exception e) { +// } +// +// @Override +// public LiveExpression getBootProjectExclusion() { +// return bootProjectExclusion; +// } +// +// @Override +// public IPropertyStore getViewProperties() { +// return viewProperties; +// } +// +// @Override +// public IPropertyStore getPrivatePropertyStore() { +// return privateProperties; +// } +// +// @Override +// public BootInstallManager getBootInstallManager() { +// return bootI +// } +// } + + public BootDashModel getRunTargetModel(RunTargetType type) { + List models = getRunTargetModels(type); + Assert.assertEquals(1, models.size()); + return models.get(0); + } + + public LocalBootDashModel getLocalTargetModel() { + return (LocalBootDashModel) getRunTargetModel(RunTargetTypes.LOCAL); + } + + public List getRunTargetModels(RunTargetType type) { + ArrayList models = new ArrayList<>(); + for (BootDashModel m : model.getSectionModels().getValue()) { + if (m.getRunTarget().getType().equals(type)) { + models.add(m); + } + } + return Collections.unmodifiableList(models); + } + + public void dispose() { + model.dispose(); + } + + public ImmutableSet getRunTargets() { + return model.getRunTargets().getValues(); + } + + public RunTarget getRunTarget(RunTargetType targetType) { + List targets = getRunTargets(targetType); + Assert.assertEquals(1, targets.size()); + return targets.get(0); + } + + private List getRunTargets(RunTargetType targetType) { + ArrayList list = new ArrayList<>(); + for (RunTarget runTarget : model.getRunTargets().getValues()) { + if (runTarget.getType().equals(targetType)) { + list.add(runTarget); + } + } + return list; + } + + public BootDashElement getElementWithName(String name) { + BootDashElement found = null; + for (BootDashModel section : model.getSectionModels().getValue()) { + for (BootDashElement e : section.getElements().getValues()) { + if (name.equals(e.getName())) { + assertNull("Found more than one element with name '"+name+"'", found); + found = e; + } + } + } + assertNotNull("No element with name '"+name+"'", found); + return found; + } + + public BootDashElement getElementFor(ILaunchConfiguration conf) { + LocalBootDashModel localSection = (LocalBootDashModel) model.getSectionByTargetId(RunTargets.LOCAL.getId()); + return localSection.getLaunchConfElementFactory().createOrGet(conf); + } + + public BootProjectDashElement getElementFor(IProject project) { + LocalBootDashModel localSection = (LocalBootDashModel) model.getSectionByTargetId(RunTargets.LOCAL.getId()); + return localSection.getProjectElementFactory().createOrGet(project); + } + + public static void assertOk(LiveExpression validator) { + ValidationResult status = validator.getValue(); + if (!status.isOk()) { + fail(status.toString()); + } + } + + public void assertLabelContains(String expectSnippet, Object element) { + assertContains(expectSnippet, getLabel(element)); + } + + public void assertLabelNotContains(String expectSnippet, GenericRemoteAppElement element) { + String label = getStyledLabel(element).getString(); + if (label.contains(expectSnippet)) { + fail("Found unexpected '"+expectSnippet+"' in '"+label); + } + } + + public void assertLabelContains(String expectSnippet, Color expectedColor, GenericRemoteAppElement element) { + StyledString label = getStyledLabel(element); + int snippetStart = label.getString().indexOf(expectSnippet); + int snippetEnd = snippetStart+expectSnippet.length(); + assertTrue("Not found '"+expectSnippet+"' in '"+label.getString()+"'", snippetStart>=0); + StyleRange[] styles = label.getStyleRanges(); + for (StyleRange r : styles) { + int end = r.start + r.length; + boolean overlaps = r.start<=snippetStart && end >= snippetEnd; + if (overlaps) { + if (expectedColor.equals(r.foreground)) { + return; // found it! + } + } + } + fail("Snippet found but not the right color"); + } + + + public StyledString getStyledLabel(Object element) { + Stylers stylers = new Stylers(null); + try (BootDashLabels labels = new BootDashLabels(context.injections, stylers)) { + try { + return labels + .getStyledText(element, BootDashColumn.TREE_VIEWER_MAIN); + } finally { + stylers.dispose(); + } + } + } + + public String getLabel(Object element) { + return getStyledLabel(element).getString(); + } + + public ButtonModel assertButton(BootDashModel model, String expectedLabel) { + StringBuilder labels = new StringBuilder(); + for (ButtonModel button : model.getButtons().getValues()) { + labels.append(button.getLabel()+"\n"); + if (expectedLabel.equals(button.getLabel())) { + return button; + } + } + throw new AssertionFailedError("Label not found: "+expectedLabel+"\nActual labels are:\n"+labels); + } + + public ButtonModel getButton(BootDashModel model, String label) { + for (ButtonModel button : model.getButtons().getValues()) { + if (label.equals(button.getLabel())) { + return button; + } + } + return null; + } + + @SuppressWarnings("rawtypes") + public BootDashModel getRunTargetModel(Class runTargetClass) { + RunTargetType target = context.injections.getBean(runTargetClass); + return getRunTargetModel(target); + } + + public void assertInstancesLabel(String expect, BootDashElement e) { + try (BootDashLabels labels = new BootDashLabels(context.injections, null)) { + String actual = labels.getStyledText(e, BootDashColumn.INSTANCES).toString(); + assertEquals(expect, actual); + } + } + + public void assertInstancesLabel(String expect, String expectDecluttered, BootDashElement e) { + try (BootDashLabels labels = new BootDashLabels(context.injections, null)) { + labels.setDeclutter(false); + String actual = labels.getStyledText(e, BootDashColumn.INSTANCES).toString(); + assertEquals(expect, actual); + + labels.setDeclutter(true); + String actualDecluttered = labels.getStyledText(e, BootDashColumn.INSTANCES).toString(); + assertEquals(expectDecluttered, actualDecluttered); + } + } + + public T waitForCallable(String name, long timeout, Callable callable) throws Exception { + AtomicReference result = new AtomicReference<>(); + ACondition.waitFor(name, timeout, () -> { + T _result = callable.call(); + assertNotNull(result); + result.set(_result); + }); + return result.get(); + } + + public BootProjectDashElement waitForElement(long timeout, IProject project) throws Exception { + return waitForCallable("Element for project "+project.getName(), timeout, () -> getElementFor(project)); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashViewModelTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashViewModelTest.java new file mode 100644 index 000000000..cfd8e529c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootDashViewModelTest.java @@ -0,0 +1,748 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.test; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.contains; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.assertElements; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials.CFCredentialType; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.StoreCredentialsMode; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryTargetProperties; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +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.BootProjectDashElement; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.RunTargets; +import org.springframework.ide.eclipse.boot.dash.model.SecuredCredentialsStore; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel.FilterChoice; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.TargetProperties; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockBootDashModel; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockRunTarget; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockRunTargetType; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; +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.util.Filter; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +import com.google.common.collect.ImmutableSet; + +/** + * @author Kris De Volder + */ +public class BootDashViewModelTest { + + private TestBootDashModelContext context; + private BootDashViewModelHarness harness = null; + private BootProjectTestHarness projects = new BootProjectTestHarness(ResourcesPlugin.getWorkspace()); + + @Before + public void setup() throws Exception { + context = new TestBootDashModelContext( + ResourcesPlugin.getWorkspace(), + DebugPlugin.getDefault().getLaunchManager() + ); + StsTestUtil.cleanUpProjects(); + } + + @After + public void teardown() throws Exception { + if (harness!=null) { + harness.dispose(); + } + } + + @Test + public void testCreate() throws Exception { + harness = new BootDashViewModelHarness(context.withTargetTypes(RunTargetTypes.LOCAL)); + BootDashModel localModel = harness.getRunTargetModel(RunTargetTypes.LOCAL); + assertNotNull(localModel); + + assertElements(harness.model.getRunTargets().getValues(), + RunTargets.LOCAL + ); + } + + @Test + public void testGetTargetTypes() throws Exception { + RunTargetType targetType = mock(RunTargetType.class); + harness = new BootDashViewModelHarness(context.withTargetTypes( + RunTargetTypes.LOCAL, + targetType + )); + + assertElements(harness.model.getRunTargetTypes(), + RunTargetTypes.LOCAL, + targetType + ); + + } + + @Test + public void testAddAndRemoveRunTarget() throws Exception { + RunTargetType targetType = mock(RunTargetType.class); + RunTarget target = mock(RunTarget.class); + BootDashModel bootDashModel = mock(BootDashModel.class); + + harness = new BootDashViewModelHarness(context.withTargetTypes( + RunTargetTypes.LOCAL, + targetType + )); + + assertEquals(0, harness.getRunTargetModels(targetType).size()); + + when(target.getId()).thenReturn("target_id"); + when(target.getType()).thenReturn(targetType); + when(target.createSectionModel(harness.model)).thenReturn(bootDashModel); + + when(targetType.canInstantiate()).thenReturn(true); + when(targetType.createRunTarget(any(TargetProperties.class))).thenReturn(target); + + when(bootDashModel.getRunTarget()).thenReturn(target); + + LiveSetVariable runTargets = harness.model.getRunTargets(); + + //Adding... + runTargets.add(target); + List models = harness.getRunTargetModels(targetType); + assertEquals(1, models.size()); + + BootDashModel targetModel = models.get(0); + assertEquals(target, targetModel.getRunTarget()); + + //Removing... + runTargets.remove(target); + + models = harness.getRunTargetModels(targetType); + assertEquals(0, models.size()); + + assertEquals(1, harness.getRunTargetModels(RunTargetTypes.LOCAL).size()); + } + + @Test + public void testElementStateListenerAddedAfterModel() throws Exception { + RunTargetType targetType = mock(RunTargetType.class); + RunTarget target = mock(RunTarget.class); + harness = new BootDashViewModelHarness(context.withTargetTypes( + RunTargetTypes.LOCAL, + targetType + )); + //We need a more fleshed-out BootDashModel mock for this test, so not using mockito here: + MockBootDashModel bdm = new MockBootDashModel(target, harness.context, harness.model); + + when(target.getId()).thenReturn("target_id"); + when(target.getType()).thenReturn(targetType); + when(target.createSectionModel(harness.model)).thenReturn(bdm); + + //////Add target/////// + + harness.model.getRunTargets().add(target); + + //Make sure the model got added as expected + List models = harness.getRunTargetModels(targetType); + assertEquals(1, models.size()); + + BootDashElement element = mock(BootDashElement.class); + bdm.add(element); + + /////Add listener//////// + + ElementStateListener listener = mock(ElementStateListener.class); + harness.model.addElementStateListener(listener); + + ////Fire event/////////// + + bdm.notifyElementChanged(element, "FIRST event fired by the test"); + + /////Verify listener + + verify(listener).stateChanged(element); + + //////////////////////////////////////////////////////////////////////////// + + reset(listener); + + harness.model.removeElementStateListener(listener); + bdm.notifyElementChanged(element, "SECOND event fired by the test"); + + verifyZeroInteractions(listener); + + } + + @Test + public void testElementStateListenerAddedBeforeModel() throws Exception { + RunTargetType targetType = mock(RunTargetType.class); + RunTarget target = mock(RunTarget.class); + harness = new BootDashViewModelHarness(context.withTargetTypes( + RunTargetTypes.LOCAL, + targetType + )); + //We need a more fleshed-out BootDashModel mock for this test, so not using mockito here: + MockBootDashModel bdm = new MockBootDashModel(target, harness.context, harness.model); + + when(target.getId()).thenReturn("target_id"); + when(target.getType()).thenReturn(targetType); + when(target.createSectionModel(harness.model)).thenReturn(bdm); + + /////Add listener//////// + + ElementStateListener listener = mock(ElementStateListener.class); + harness.model.addElementStateListener(listener); + + //////Add target/////// + + harness.model.getRunTargets().add(target); + + //Make sure the model got added as expected + List models = harness.getRunTargetModels(targetType); + assertEquals(1, models.size()); + + BootDashElement element = mock(BootDashElement.class); + bdm.add(element); + + ////Fire event/////////// + + bdm.notifyElementChanged(element, "event fired by the test"); + + /////Verify listener + + verify(listener).stateChanged(element); + } + + @Test + public void testRemoveTargetToleratesNull() throws Exception { + UserInteractions ui = mock(UserInteractions.class); + harness = new BootDashViewModelHarness(context.withTargetTypes( + RunTargetTypes.LOCAL + )); + harness.model.removeTarget(null, ui); + + verifyZeroInteractions(ui); + } + + @Test + public void testRemoveTargetCanceled() throws Exception { + RunTargetType targetType = mock(RunTargetType.class); + RunTarget target = mock(RunTarget.class); + harness = new BootDashViewModelHarness(context.withTargetTypes( + RunTargetTypes.LOCAL, + targetType + )); + BootDashModel bdm = new MockBootDashModel(target, harness.context, harness.model); + + when(target.getId()).thenReturn("target_id"); + when(target.getName()).thenReturn("target_name"); + when(target.getType()).thenReturn(targetType); + when(target.createSectionModel(harness.model)).thenReturn(bdm); + + //////Add target/////// + harness.model.getRunTargets().add(target); + + //Make sure the model got added as expected + List models = harness.getRunTargetModels(targetType); + assertEquals(1, models.size()); + + /////Remove target operation CANCELED //// + + UserInteractions ui = mock(UserInteractions.class); + when(ui.confirmOperation( + contains("Deleting run target: target_name"), + contains("Are you sure") + )).thenReturn(false); + + harness.model.removeTarget(target, ui); + + //Since user canceled, the target should NOT actually have been removed + assertTrue(harness.model.getRunTargets().contains(target)); + +// verify(ui).confirmOperation(anyString(), anyString()); +// verifyNoMoreInteractions(ui); + } + + @Test + public void testRemoveTargetConfirmed() throws Exception { + RunTargetType targetType = mock(RunTargetType.class); + RunTarget target = mock(RunTarget.class); + harness = new BootDashViewModelHarness(context.withTargetTypes( + RunTargetTypes.LOCAL, + targetType + )); + BootDashModel bdm = new MockBootDashModel(target, harness.context, harness.model); + + when(target.getId()).thenReturn("target_id"); + when(target.getName()).thenReturn("target_name"); + when(target.getType()).thenReturn(targetType); + when(target.createSectionModel(harness.model)).thenReturn(bdm); + + //////Add target/////// + harness.model.getRunTargets().add(target); + + //Make sure the model got added as expected + List models = harness.getRunTargetModels(targetType); + assertEquals(1, models.size()); + + /////Remove target operation CANCELED //// + + UserInteractions ui = mock(UserInteractions.class); + when(ui.confirmOperation( + contains("Deleting run target: target_name"), + contains("Are you sure") + )).thenReturn(true); + + harness.model.removeTarget(target, ui); + + //Since user confirmed, the target should have been removed + assertFalse(harness.model.getRunTargets().contains(target)); + +// verify(ui).confirmOperation(anyString(), anyString()); +// verifyNoMoreInteractions(ui); + } + + + @Test + public void testRemoveTargetToleratesRemovingNonContainedElement() throws Exception { + RunTargetType targetType = mock(RunTargetType.class); + RunTarget target = mock(RunTarget.class); + RunTarget otherTarget = mock(RunTarget.class); + + harness = new BootDashViewModelHarness(context.withTargetTypes( + RunTargetTypes.LOCAL, + targetType + )); + BootDashModel bdm = new MockBootDashModel(target, harness.context, harness.model); + + when(target.getId()).thenReturn("target_id"); + when(target.getName()).thenReturn("target_name"); + when(target.getType()).thenReturn(targetType); + when(target.createSectionModel(harness.model)).thenReturn(bdm); + + when(otherTarget.getId()).thenReturn("other_id"); + + //////Add target/////// + harness.model.getRunTargets().add(target); + + //Make sure the model got added as expected + List models = harness.getRunTargetModels(targetType); + assertEquals(1, models.size()); + + /////Remove target operation //// + + UserInteractions ui = mock(UserInteractions.class); + + ImmutableSet targetsBefore = harness.getRunTargets(); + int numTargetsBefore = targetsBefore.size(); + + harness.model.removeTarget(otherTarget, ui); + ImmutableSet targetsAfter = harness.getRunTargets(); + + //Since target is not in the model, nothing should happen. + assertEquals(targetsBefore, targetsAfter); + assertEquals(numTargetsBefore, targetsAfter.size()); + verifyZeroInteractions(ui); + +// verify(ui).confirmOperation(anyString(), anyString()); +// verifyNoMoreInteractions(ui); + } + + @Test + public void testFilterBox() throws Exception { + //Note: this test check the relationship between + // filterBox and filter. It does so indirectly by observing + // that changing the filterText makes the filter behave as expected. + //However, it is not (intended to be) an in-depth test of the way the + // filter matches elements. So it only does some basic test cases. + //There are more in-depth tests for the filters elsewhere. + + harness = new BootDashViewModelHarness(context.withTargetTypes( + RunTargetTypes.LOCAL + )); + + LiveVariable filterText = harness.model.getFilterBox().getText(); + LiveExpression> filter = harness.model.getFilter(); + + assertEquals("", filterText.getValue()); + assertFilterAccepts(true, filter, "a-tag"); + + filterText.setValue("foo"); + assertFilterAccepts(false, filter, "a-tag"); + assertFilterAccepts(true, filter, "foo"); + } + + @Test + public void testToggleFilters() throws Exception { + //Note: this test check the relationship between + // filterBox and filter. It does so indirectly by observing + // that changing the filterText makes the filter behave as expected. + //However, it is not (intended to be) an in-depth test of the way the + // filter matches elements. So it only does some basic test cases. + //There are more in-depth tests for the filters elsewhere. + + harness = new BootDashViewModelHarness(context.withTargetTypes( + RunTargetTypes.LOCAL + )); + + LiveSetVariable toggleFilters = harness.model.getToggleFilters().getSelectedFilters(); + toggleFilters.replaceAll(ImmutableSet.of()); + LiveExpression> filter = harness.model.getFilter(); + + assertTrue(toggleFilters.getValue().isEmpty()); + assertFilterAccepts(true, filter, "a-tag"); + + FilterChoice toggleFilter = new FilterChoice("foo", "Foo filter", new Filter() { + public boolean accept(BootDashElement t) { + return t.getTags().contains("foo"); + } + }); + toggleFilters.add(toggleFilter); + assertFilterAccepts(false, filter, "app-name", "a-tag"); + assertFilterAccepts(true, filter, "app-name", "foo"); + } + + @Test + public void testFilterIsTreeAware() throws Exception { + harness = new BootDashViewModelHarness(context.reload().withTargetTypes( + RunTargetTypes.LOCAL + )); + + LiveVariable filterBox = harness.model.getFilterBox().getText(); + LiveExpression> filter = harness.model.getFilter(); + + IProject project = projects.createBootProject("parent"); + ILaunchConfiguration conf1 = BootLaunchConfigurationDelegate.createConf(project); + ILaunchConfiguration conf2 = BootLaunchConfigurationDelegate.createConf(project); + + BootProjectDashElement vader = harness.getElementFor(project); + BootDashElement luke = harness.getElementFor(conf1); + BootDashElement leia = harness.getElementFor(conf2); + + setTags(vader, "vader"); + setTags(luke, "luke"); + setTags(leia, "leia"); + + //Blank filter set, filter matches anything + filterBox.setValue(""); + assertFilterAccepts(true, filter, vader, luke, leia); + + //Filter non-existing tag matches nothing + filterBox.setValue("not-exist"); + assertFilterAccepts(false, filter, vader, luke, leia); + + //Filter matching a parent also causes children to match + filterBox.setValue("vader"); + assertFilterAccepts(true, filter, vader, luke, leia); + + //Filter matching a child also causes parent to match... + filterBox.setValue("luke"); + assertFilterAccepts(true, filter, vader, luke); + //... but not siblings + assertFilterAccepts(false, filter, leia); + + } + + private void assertFilterAccepts(boolean expected, LiveExpression> filter, BootDashElement... elements) { + for (BootDashElement bde : elements) { + assertEquals("element '"+bde.getName()+"'with tags "+bde.getTags(), expected, filter.getValue().accept(bde)); + } + } + + private void setTags(BootDashElement element, String... tags) { + element.setTags(new LinkedHashSet<>(Arrays.asList(tags))); + } + + private void assertFilterAccepts(boolean expectedAccept, LiveExpression> filter, String elementName, String... tags) { + BootDashElement element = mock(BootDashElement.class); + when(element.getName()).thenReturn(elementName); + when(element.getCurrentChildren()).thenReturn(ImmutableSet.of()); + when(element.getTags()).thenReturn(new LinkedHashSet<>(Arrays.asList(tags))); + assertEquals(expectedAccept, filter.getValue().accept(element)); + } + + @Test + public void testGetSectionByTargetId() throws Exception { + BootDashViewModel view = mock(BootDashViewModel.class); + LiveSetVariable sections = new LiveSetVariable<>(); + when(view.getSectionModels()).thenReturn(sections); + when(view.getSectionByTargetId(anyString())).thenCallRealMethod(); + + assertNull(view.getSectionByTargetId("some-id")); + + BootDashModel bdm = mockBDMWithTargetId("some-id"); + BootDashModel other_bdm = mockBDMWithTargetId("other-id"); + + sections.add(bdm); + sections.add(other_bdm); + + assertEquals(bdm, view.getSectionByTargetId("some-id")); + assertEquals(other_bdm, view.getSectionByTargetId("other-id")); + assertEquals(null, view.getSectionByTargetId("not-found-id")); + } + + private BootDashModel mockBDMWithTargetId(String id) { + BootDashModel bdm = mock(BootDashModel.class); + RunTarget target = mock(RunTarget.class); + when(bdm.getRunTarget()).thenReturn(target); + when(target.getId()).thenReturn(id); + return bdm; + } + + + @Test + public void testRestoreSingleRunTarget() throws Exception { + context.injections.def(MockRunTargetType.class, injections -> { + MockRunTargetType targetType = new MockRunTargetType(injections, "MOCK"); + return targetType; + }); + + String targetId = "foo"; + + harness = new BootDashViewModelHarness(context); + MockRunTargetType targetType = context.injections.getBean(MockRunTargetType.class); + + CloudFoundryTargetProperties props = new CloudFoundryTargetProperties(null, targetType, context.injections); + props.put(TargetProperties.RUN_TARGET_ID, targetId); + props.put("describe", "This is foo"); + RunTarget savedTarget = targetType.createRunTarget(props); + harness.model.getRunTargets().add(savedTarget); + BootDashModelContext oldContext = harness.context; + + harness.reload(); + + MockRunTarget restoredTarget = (MockRunTarget)harness.getRunTarget(targetType); + + //Not a stric requirement, but it would be a little strange of the restored + // target was the exact same object as the saved target (the test may be broken + // or some state in the model is not cleaned up when it is disposed) + assertTrue(restoredTarget != savedTarget); + + assertEquals(savedTarget, restoredTarget); + assertEquals("This is foo", restoredTarget.get("describe")); + } + + @Test + public void testModelComparator() throws Exception { + //View model is expected to provide a comparator that is based on + // the list of target-types it is initialized with. It should sort + //models based on type first then runtarget id second. + + context.withTargetTypes(RunTargetTypes.LOCAL); + context.injections.def(RunTargetType.class, injections -> new MockRunTargetType(context.injections, "foo-type")); + context.injections.def(RunTargetType.class, injections -> new MockRunTargetType(context.injections, "bar-type")); + harness = new BootDashViewModelHarness(context); + + Comparator comparator = harness.model.getModelComparator(); + + RunTargetType fooType = context.getRargetTypeWithName("foo-type"); + RunTargetType barType = context.getRargetTypeWithName("bar-type"); + + BootDashModel[] sortedModels = { + //These are arranged in the order we expect them to be when + // sorted properly + + harness.getRunTargetModel(RunTargetTypes.LOCAL), //first! + + //foo comes before bar (not alphabetic but based on BDVM construction) + sortableModel(fooType, "different"), //ids are alphabetic within each type + sortableModel(fooType, "other"), + sortableModel(fooType, "something"), + + sortableModel(barType, "different"), + sortableModel(barType, "other"), + sortableModel(barType, "something") + }; + + //Create a array with same elements in the wrong order... + int len = sortedModels.length; + BootDashModel[] reverseModels = new BootDashModel[len]; + for (int i = 0; i < reverseModels.length; i++) { + reverseModels[i] = sortedModels[len - i - 1]; + } + + assertFalse(reverseModels[0].equals(sortedModels[0])); //Sanity check + + Arrays.sort(reverseModels, comparator); + + assertArrayEquals(sortedModels, reverseModels); + } + + /** + * Create mock BootDashModel that is sufficiently fleshed-out to + * allow sorting using the comparator provided by BootDashViewModel + */ + private BootDashModel sortableModel(RunTargetType type, String displayName) { + BootDashModel model = mock(BootDashModel.class); + RunTarget target = mock(RunTarget.class); + + when(target.getType()).thenReturn(type); + when(model.getRunTarget()).thenReturn(target); + when(target.getDisplayName()).thenReturn(displayName); + + return model; + } + + @Test + public void testUpdatePropertiesInStore() throws Exception { + context.injections.def(MockRunTargetType.class, injections -> { + MockRunTargetType targetType = new MockRunTargetType(injections, "mock-type"); + targetType.setRequiresCredentials(true); + return targetType; + }); + harness = new BootDashViewModelHarness(context); + + MockRunTargetType targetType = context.injections.getBean(MockRunTargetType.class); + CloudFoundryTargetProperties properties = new CloudFoundryTargetProperties(null, targetType, context.injections); + properties.setCredentials(CFCredentials.fromPassword("secret")); + + MockRunTarget target = (MockRunTarget) targetType.createRunTarget(properties); + harness.model.getRunTargets().add(target); + + harness.model.updateTargetPropertiesInStore(); + + assertEquals("secret", ((CloudFoundryTargetProperties)target.getTargetProperties()).getCredentials().getSecret()); + + harness.reload(); + + MockRunTarget restoredTarget = (MockRunTarget) harness.getRunTarget(targetType); + assertTrue(restoredTarget != target); //Not a strict requirement, but it is more or less + // expected the restored target is a brand new object + + //TODO: Strange test. Shouldn't there be something to check here? + } + + @Test + public void testRememberPassword() throws Exception { + context.injections.def(MockRunTargetType.class, injections -> { + MockRunTargetType targetType = new MockRunTargetType(injections, "mock-type"); + targetType.setRequiresCredentials(true); + return targetType; + }); + harness = new BootDashViewModelHarness(context); + MockRunTargetType targetType = context.injections.getBean(MockRunTargetType.class); + CloudFoundryTargetProperties properties = new CloudFoundryTargetProperties(null, targetType, context.injections); + properties.put(TargetProperties.RUN_TARGET_ID, "target-id"); + properties.setStoreCredentials(StoreCredentialsMode.STORE_PASSWORD); + properties.setCredentials(CFCredentials.fromPassword("secret")); + + MockRunTarget target = (MockRunTarget) targetType.createRunTarget(properties); + harness.model.getRunTargets().add(target); + + harness.model.updateTargetPropertiesInStore(); + + SecuredCredentialsStore secureStore = harness.context.getSecuredCredentialsStore(); + + //This test needs to have knowledge what keys the passwords are store under. + // That seems undesirable. + String key = "mock-type:target-id"; + { + CloudFoundryTargetProperties targetProperties = (CloudFoundryTargetProperties) target.getTargetProperties(); + assertEquals(StoreCredentialsMode.STORE_PASSWORD, targetProperties.getStoreCredentials()); + assertEquals(CFCredentialType.PASSWORD, targetProperties.getCredentials().getType()); + assertEquals("secret", targetProperties.getCredentials().getSecret()); + assertEquals("secret", secureStore.getCredentials(key)); + } + + ///////////////////////////////////////// + // check that when runtargets are restored from the store the password prop is properly + // restored + + harness.reload(); + + MockRunTarget restoredTarget = (MockRunTarget) harness.getRunTarget(targetType); + assertTrue(restoredTarget != target); //Not a strict requirement, but it is more or less + // expected the restored target is a brand new object + { + CloudFoundryTargetProperties restoredTargetProperties = (CloudFoundryTargetProperties) restoredTarget.getTargetProperties(); + assertEquals(StoreCredentialsMode.STORE_PASSWORD, restoredTargetProperties.getStoreCredentials()); + assertEquals("secret", restoredTargetProperties.getCredentials().getSecret()); + } + } + + @Test + public void testDontRememberPassword() throws Exception { + context.injections.def(MockRunTargetType.class, injections -> { + MockRunTargetType targetType = new MockRunTargetType(injections, "mock-type"); + targetType.setRequiresCredentials(true); + return targetType; + }); + harness = new BootDashViewModelHarness(context); + + MockRunTargetType targetType = context.injections.getBean(MockRunTargetType.class); + CloudFoundryTargetProperties properties = new CloudFoundryTargetProperties(null, targetType, context.injections); + properties.setStoreCredentials(StoreCredentialsMode.STORE_NOTHING); + properties.setCredentials(CFCredentials.fromPassword("secret")); + + MockRunTarget target = (MockRunTarget) targetType.createRunTarget(properties); + harness.model.getRunTargets().add(target); + + harness.model.updateTargetPropertiesInStore(); + + SecuredCredentialsStore secureStore = harness.context.getSecuredCredentialsStore(); + + //This test needs to have knowledge what keys the passwords are store under. + // That seems undesirable. + String key = "mock-type:target-id"; + { + CloudFoundryTargetProperties targetProperties = (CloudFoundryTargetProperties) target.getTargetProperties(); + assertEquals(StoreCredentialsMode.STORE_NOTHING, targetProperties.getStoreCredentials()); + assertEquals("secret", targetProperties.getCredentials().getSecret()); + assertNull(secureStore.getCredentials(key)); + } + + ///////////////////////////////////////// + // check that when runtargets are restored from the store the password is not remebered + + harness.reload(); + + MockRunTarget restoredTarget = (MockRunTarget) harness.getRunTarget(targetType); + assertTrue(restoredTarget != target); //Not a strict requirement, but it is more or less + // expected the restored target is a brand new object + { + CloudFoundryTargetProperties restoredTargetProperties = (CloudFoundryTargetProperties) restoredTarget.getTargetProperties(); + assertEquals(StoreCredentialsMode.STORE_NOTHING,restoredTargetProperties.getStoreCredentials()); + assertNull(restoredTargetProperties.getCredentials()); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootJarPackagingTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootJarPackagingTest.java new file mode 100644 index 000000000..a12742c3d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/BootJarPackagingTest.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test; + +import static org.springsource.ide.eclipse.commons.tests.util.StsTestUtil.assertNoErrors; + +import java.io.File; +import java.util.jar.JarFile; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.mockito.Mockito; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cf.packaging.CloudApplicationArchiverStrategies; +import org.springframework.ide.eclipse.boot.dash.cf.packaging.CloudApplicationArchiverStrategy; +import org.springframework.ide.eclipse.boot.dash.cf.packaging.ICloudApplicationArchiver; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; +import org.springframework.ide.eclipse.boot.test.util.JavaUtils; +import org.springframework.ide.eclipse.boot.test.util.LaunchResult; +import org.springsource.ide.eclipse.commons.tests.util.StsTestCase; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.setPackage; + +public class BootJarPackagingTest extends StsTestCase { + + private JavaUtils java = new JavaUtils(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + StsTestUtil.deleteAllProjects(); + StsTestUtil.setAutoBuilding(false); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testSimple() throws Exception { + UserInteractions ui = Mockito.mock(UserInteractions.class); + BootProjectTestHarness harness = getHarness(); + IProject project = harness.createBootProject("simple-boot", + setPackage("demo") + ); + createFile(project, "src/main/java/demo/Greeter.java", + "package demo;\n" + + "\n" + + "import org.springframework.boot.CommandLineRunner;\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class Greeter implements CommandLineRunner {\n" + + "\n" + + " @Override\n" + + " public void run(String... arg0) throws Exception {\n" + + " System.out.println(\"Hello World!\");\n" + + " }\n" + + "\n" + + "}\n" + ); + StsTestUtil.assertNoErrors(project); // Builds the project + File jarFile = packageAsJar(project, ui); + LaunchResult result = java.runJar(jarFile); + assertContains("Hello World!", result.out); + assertEquals(0, result.terminationCode); + Mockito.verifyZeroInteractions(ui); + } + + public void testProjectDependency() throws Exception { + IProject depProject = createPredefinedProject("demo-lib"); + StsTestUtil.assertNoErrors(depProject); + UserInteractions ui = Mockito.mock(UserInteractions.class); + BootProjectTestHarness harness = getHarness(); + + IProject project = harness.createBootProject("simple-boot", + setPackage("demo") + ); + fileReplace(project, "pom.xml", + "", + " \n" + + " org.demo\n" + + " demo-lib\n" + + " 0.0.1\n" + + " \n" + + " "); + + + createFile(project, "src/main/java/demo/Greeter.java", + "package demo;\n" + + "\n" + + "import org.demo.lib.TheLib;\n" + + "import org.springframework.boot.CommandLineRunner;\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class Greeter implements CommandLineRunner {\n" + + "\n" + + " @Override\n" + + " public void run(String... arg0) throws Exception {\n" + + " System.out.println(TheLib.createGreeting(\"Kris\"));\n" + + " }\n" + + "\n" + + "}\n" + ); + assertNoErrors(project); + + File jarFile = packageAsJar(project, ui); + assertEntries(jarFile, + "lib/demo-lib.jar" + ); + LaunchResult result = java.runJar(jarFile); + assertContains("Hello, Kris!", result.out); + assertEquals(0, result.terminationCode); + + Mockito.verifyZeroInteractions(ui); + } + + /** + * Verifies that jarFile has at least a given list of entries (in any order). + */ + private void assertEntries(File file, String... expected) throws Exception { + JarFile jarFile = new JarFile(file); + try { + for (String name : expected) { + assertNotNull("Missing expected Jar Entry: "+name, jarFile.getJarEntry(name)); + } + } finally { + jarFile.close(); + } + } + + public static File packageAsJar(IProject project, UserInteractions ui) throws Exception { + CloudApplicationArchiverStrategy strategy = CloudApplicationArchiverStrategies.packageAsJar(project, ui); + ICloudApplicationArchiver archiver = strategy.getArchiver(new NullProgressMonitor()); + assertNotNull(archiver); + File jar = archiver.getApplicationArchive(new NullProgressMonitor()); + assertNotNull(jar); + assertTrue(jar.isFile()); + return jar; + } + + private BootProjectTestHarness getHarness() { + return new BootProjectTestHarness(ResourcesPlugin.getWorkspace()); + } + + @Override + protected String getBundleName() { + return BootDashTestBundleConstants.BUNDLE_ID; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CfTestTargetParams.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CfTestTargetParams.java new file mode 100644 index 000000000..65542a15a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CfTestTargetParams.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.eclipse.core.runtime.Assert; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFClientParams; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springsource.ide.eclipse.commons.frameworks.core.util.StringUtils; + +/** + * @author Kris De Volder + */ +public class CfTestTargetParams { + + public static CFClientParams fromEnv() { + try { + return new CFClientParams( + fromEnv("CF_TEST_API_URL"), + fromEnv("CF_TEST_USER"), + CFCredentials.fromPassword(fromEnv("CF_TEST_PASSWORD")), + false, //self signed + fromEnv("CF_TEST_ORG"), + fromEnvOrFile("CF_TEST_SPACE"), + fromEnvBoolean("CF_TEST_SKIP_SSL") + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static String fromEnvOrFile(String name) throws IOException { + String envVal = getEnvMaybe(name); + if (envVal!=null) { + return envVal; + } else { + String fileName = fromEnv(name+"_FILE"); + Path path = FileSystems.getDefault().getPath(fileName); + return Files.readAllLines(path).get(0); + } + } + + private static String getEnvMaybe(String name) { + String val = System.getenv(name); + if (!StringUtils.hasText(val)) { + val = System.getenv("bamboo_"+name); + } + return val; + } + + public static String fromEnv(String name) { + String value = getEnvMaybe(name); + Assert.isLegal(StringUtils.hasText(value), "The environment varable '"+name+"' must be set"); + return value; + } + + private static boolean fromEnvBoolean(String name) { + String value = getEnvMaybe(name); + if (value == null) { + return false; + } + return Boolean.parseBoolean(value); + } + + public static CFClientParams fromEnvWithCredentials(CFCredentials credentials) { + try { + return new CFClientParams( + fromEnv("CF_TEST_API_URL"), + fromEnv("CF_TEST_USER"), + credentials, + false, //self signed + fromEnv("CF_TEST_ORG"), + fromEnvOrFile("CF_TEST_SPACE"), + fromEnvBoolean("CF_TEST_SKIP_SSL") + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryApplicationHarness.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryApplicationHarness.java new file mode 100644 index 000000000..8bd96399a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryApplicationHarness.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.junit.Assert.fail; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.DefaultClientRequestsV2; +import org.springframework.ide.eclipse.boot.dash.console.IApplicationLogConsole; +import org.springframework.ide.eclipse.boot.dash.console.LogMessage; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +import reactor.core.Disposable; + +public class CloudFoundryApplicationHarness extends AbstractDisposable { + + private Set ownedAppNames = new HashSet<>(); + private DefaultClientRequestsV2 client; + + public CloudFoundryApplicationHarness(DefaultClientRequestsV2 client) { + this.client = client; + if (client!=null) { + onDispose((d) -> { deleteOwnedApps(); }); + } + } + + public String randomAppName() throws Exception { + String name = StringUtil.datestamp()+"-"+randomAlphabetic(10); + ownedAppNames.add(name); + streamOutput(name); + return name; + } + + private void streamOutput(String name) throws Exception { + if (client!=null) { + IApplicationLogConsole logConsole = new IApplicationLogConsole() { + + @Override + public void onMessage(LogMessage log) { + System.out.println("%"+name+"-out: "+log.getMessage()); + } + + @Override + public void onComplete() { + System.out.println("%"+name+"-COMPLETE"); + } + + @Override + public void onError(Throwable exception) { + System.out.println("%"+name+"-ERROR: "+ExceptionUtil.getMessage(exception)); + } + + }; + Disposable logToken = client.streamLogs(name, logConsole); + onDispose((d) -> { + logToken.dispose(); + }); + } + } + + protected void deleteOwnedApps() { + System.out.println("owned app names: "+ownedAppNames); + if (!ownedAppNames.isEmpty()) { + + try { + for (String name : ownedAppNames) { + try { + System.out.println("delete owned app: "+name); + this.client.deleteApplication(name); + } catch (Exception e) { + System.out.println("Delete failed: " +ExceptionUtil.getMessage(e)); + // May get 404 or other 400 errors if it is alrready + // gone so don't prevent other owned apps from being + // deleted + } + } + + } catch (Exception e) { + fail("failed to cleanup owned apps: " + e.getMessage()); + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryBootDashModelIntegrationTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryBootDashModelIntegrationTest.java new file mode 100644 index 000000000..e85defc16 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryBootDashModelIntegrationTest.java @@ -0,0 +1,606 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.contains; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.ide.eclipse.boot.dash.test.BootDashModelTest.waitForJobsToComplete; +import static org.springframework.ide.eclipse.boot.dash.test.CloudFoundryTestHarness.APP_DELETE_TIMEOUT; +import static org.springframework.ide.eclipse.boot.dash.test.CloudFoundryTestHarness.APP_DEPLOY_TIMEOUT; +import static org.springframework.ide.eclipse.boot.dash.test.CloudFoundryTestHarness.APP_IS_VISIBLE_TIMEOUT; +import static org.springframework.ide.eclipse.boot.dash.test.CloudFoundryTestHarness.SERVICE_DELETE_TIMEOUT; +import static org.springframework.ide.eclipse.boot.dash.test.util.JobUtil.runInJob; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.withStarters; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.createFile; + +import java.io.File; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.apache.commons.io.IOUtils; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Assert; +import org.eclipse.debug.core.DebugPlugin; +import org.junit.After; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFClientParams; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFServiceInstance; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials.CFCredentialType; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.DefaultClientRequestsV2; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.DefaultCloudFoundryClientFactoryV2; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.StoreCredentialsMode; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudServiceInstanceDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement; +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.test.util.SslValidationDisabler; +import org.springframework.ide.eclipse.boot.test.AutobuildingEnablement; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; +import org.springframework.ide.eclipse.boot.test.util.TestBracketter; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * @author Kris De Volder + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class CloudFoundryBootDashModelIntegrationTest { + + private TestBootDashModelContext context; + private BootProjectTestHarness projects; + private CloudFoundryTestHarness harness; + + //////////////////////////////////////////////////////////// + private CFClientParams clientParams = CfTestTargetParams.fromEnv(); + private DefaultCloudFoundryClientFactoryV2 clientFactory = DefaultCloudFoundryClientFactoryV2.INSTANCE; + private DefaultClientRequestsV2 client = CloudFoundryClientTest.createClient(clientParams); + + public CloudFoundryApplicationHarness appHarness = new CloudFoundryApplicationHarness(client); + + @Rule + public AutobuildingEnablement disableAutoBuild = new AutobuildingEnablement(false); + + @Rule + public TestBracketter testBracketter = new TestBracketter(); + + public CloudFoundryServicesHarness services = new CloudFoundryServicesHarness(clientParams, client); + + @Before + public void setup() throws Exception { + SslValidationDisabler.disableSslValidation(); + StsTestUtil.deleteAllProjects(); + this.context = new TestBootDashModelContext( + ResourcesPlugin.getWorkspace(), + DebugPlugin.getDefault().getLaunchManager() + ).withCfClient(clientFactory); + this.harness = CloudFoundryTestHarness.create(context); + this.projects = new BootProjectTestHarness(context.getWorkspace()); + } + + @After + public void tearDown() throws Exception { + appHarness.deleteOwnedApps(); + services.dispose(); + client.close(); + harness.dispose(); + } + + //////////////////////////////////////////////////////////// + + @Test + public void testCreateCfTarget() throws Exception { + CloudFoundryBootDashModel target = harness.createCfTarget(CfTestTargetParams.fromEnv()); + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertNotNull(target); + assertEquals(CFCredentialType.PASSWORD, credentials.getType()); + assertNotNull(credentials.getSecret()); + assertEquals(1, harness.getCfRunTargetModels().size()); + } + + @Test public void refreshTokenStreamTerminatedOnDispose() throws Exception { + CFClientParams params = CfTestTargetParams.fromEnv(); + ClientRequests client = clientFactory.getClient(params); + + Future> tokens = runInJob(() -> { + return client.getRefreshTokens().collect(Collectors.toList()).block(); + }); + + client.getApplicationsWithBasicInfo(); // forces the client to authenticate + client.dispose(); + + assertEquals(1, tokens.get(2, TimeUnit.SECONDS).size()); + } + + @Test public void testCreateCfTargetAndStoreToken() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + + CloudFoundryBootDashModel target = harness.createCfTarget(targetParams, StoreCredentialsMode.STORE_TOKEN); + assertNotNull(target); + assertTrue(target.isConnected()); + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertEquals(CFCredentialType.REFRESH_TOKEN, credentials.getType()); + assertNotNull(credentials.getSecret()); + assertEquals(1, harness.getCfRunTargetModels().size()); + } + + + /** + * Test that tests a bunch of things. + * TODO: It isn't good practice to create 'test everything' tests... + * but we do it anyway because ramping up a test that deploys an app takes about 90 seconds... + * Maybe we can factor this better somehow so we have separate tests, but only deploy app once? + */ + @Test + public void testDeployAppAndDeleteAndStuff() throws Exception { + harness.createCfTarget(CfTestTargetParams.fromEnv()); + final CloudFoundryBootDashModel model = harness.getCfTargetModel(); + + final BootProjectDashElement project = harness.getElementFor( + projects.createBootProject("to-deploy", withStarters("actuator", "web")) + ); + final String appName = appHarness.randomAppName(); + + harness.answerDeploymentPrompt(ui(), appName, appName); + model.add(ImmutableList.of(project)); + + //The resulting deploy is asynchronous + new ACondition("wait for app '"+ appName +"'to appear", APP_IS_VISIBLE_TIMEOUT) { + public boolean test() throws Exception { + assertNotNull(model.getApplication(appName)); + return true; + } + }; + + new ACondition("wait for app '"+ appName +"'to be RUNNING", APP_DEPLOY_TIMEOUT) { + public boolean test() throws Exception { + CloudAppDashElement element = model.getApplication(appName); + assertEquals(RunState.RUNNING, element.getRunState()); + return true; + } + }; + + //Try to get request mappings + //TODO: make this work again in recent boot version (the rm endpoints are now not accessible anymore over http/https by default + // must actuator + ssh tunnel +// new ACondition("wait for request mappings", FETCH_REQUEST_MAPPINGS_TIMEOUT) { +// public boolean test() throws Exception { +// CloudAppDashElement element = model.getApplication(appName); +// List mappings = element.getLiveRequestMappings(); +// assertNotNull(mappings); //Why is the test sometimes failing here? +// assertTrue(!mappings.isEmpty()); //Even though this is an 'empty' app should have some mappings, +// // for example an 'error' page. +// return true; +// } +// }; + + //Try to delete the app... + reset(ui()); + when(ui().confirmOperation(eq("Deleting Elements"), anyString())).thenReturn(true); + + CloudAppDashElement app = model.getApplication(appName); + app.getBootDashModel().delete(ImmutableList.of(app), ui()); + + new ACondition("wait for app to be deleted", APP_DELETE_TIMEOUT) { + + @Override + public boolean test() throws Exception { + assertNull(model.getApplication(appName)); + return true; + } + }; + } + + private AllUserInteractions ui() { + return context.injections.getBean(AllUserInteractions.class); + } + + @Test public void testDeployAppIntoDebugMode() throws Exception { + harness.createCfTarget(CfTestTargetParams.fromEnv()); + final CloudFoundryBootDashModel model = harness.getCfTargetModel(); + + final BootProjectDashElement project = harness.getElementFor( + projects.createBootProject("to-deploy", withStarters("actuator", "web")) + ); + final String appName = appHarness.randomAppName(); + + harness.answerDeploymentPrompt(ui(), appName, appName); + model.performDeployment(ImmutableSet.of(project.getProject()), RunState.DEBUGGING); + + new ACondition("wait for app '"+ appName +"'to be DEBUGGING", APP_DEPLOY_TIMEOUT) { + public boolean test() throws Exception { + CloudAppDashElement element = model.getApplication(appName); + assertEquals(RunState.DEBUGGING, element.getRunState()); + return true; + } + }; + + } + +//This test commented because it uses 'createApplication' which no longer exists in V2 client. +// @Test +// public void testPreexistingApplicationInModel() throws Exception { +// // Create external client and deploy app "externally" +// ClientRequests externalClient = harness.createExternalClient(CfTestTargetParams.fromEnv()); +// +// List domains = externalClient.getDomains(); +// +// final String preexistingAppName = harness.randomAppName(); +// +// CloudApplicationDeploymentProperties deploymentProperties = new CloudApplicationDeploymentProperties(); +// deploymentProperties.setAppName(preexistingAppName); +// deploymentProperties.setMemory(1024); +// deploymentProperties.setUris(ImmutableList.of(preexistingAppName + "." + domains.get(0).getName())); +// deploymentProperties.setServices(ImmutableList.of()); +// externalClient.createApplication(deploymentProperties); +// +// // Create the boot dash target and model +// harness.createCfTarget(CfTestTargetParams.fromEnv()); +// +// final CloudFoundryBootDashModel model = harness.getCfTargetModel(); +// +// final BootProjectDashElement project = harness +// .getElementFor(projects.createBootWebProject("testPreexistingApplicationInModel")); +// final String newAppName = harness.randomAppName(); +// +// // Create a new one too +// harness.answerDeploymentPrompt(ui(), newAppName, newAppName); +// +// model.add(ImmutableList. of(project), ui); +// +// // The resulting deploy is asynchronous +// new ACondition("wait for apps '" + newAppName + "' and '" + preexistingAppName + "' to appear", +// APP_IS_VISIBLE_TIMEOUT) { +// public boolean test() throws Exception { +// assertNotNull(model.getApplication(newAppName)); +// assertNotNull(model.getApplication(preexistingAppName)); +// +// // check project mapping +// assertEquals("Expected new element in model to have workspace project mapping", +// model.getApplication(newAppName).getProject(), project.getProject()); +// +// // No project mapping for the "external" app +// assertNull(model.getApplication(preexistingAppName).getProject()); +// +// // check the actual CloudApplication +// CFApplication actualNewApp = model.getApplication(newAppName).getSummaryData(); +// assertEquals("No CloudApplication mapping found", actualNewApp.getName(), newAppName); +// +// CFApplication actualPreexistingApp = model.getApplication(preexistingAppName).getSummaryData(); +// assertEquals("No CloudApplication mapping found", actualPreexistingApp.getName(), preexistingAppName); +// +// return true; +// } +// }; +// } + + @Test + public void testEnvVarsSetOnFirstDeploy() throws Exception { + CloudFoundryBootDashModel target = harness.createCfTarget(CfTestTargetParams.fromEnv()); + final CloudFoundryBootDashModel model = harness.getCfTargetModel(); + + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + + final String appName = appHarness.randomAppName(); + + Map env = new HashMap<>(); + env.put("FOO", "something"); + harness.answerDeploymentPrompt(ui(), appName, appName, env); + + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + new ACondition("wait for app '"+ appName +"'to be RUNNING", APP_DEPLOY_TIMEOUT) { + public boolean test() throws Exception { + CloudAppDashElement element = model.getApplication(appName); + assertEquals(RunState.RUNNING, element.getRunState()); + return true; + } + }; + + Map actualEnv = harness.fetchEnvironment(target, appName); + + assertEquals("something", actualEnv.get("FOO")); + } + + + @Test + public void testServicesBoundOnFirstDeploy() throws Exception { + CloudFoundryBootDashModel target = harness.createCfTarget(CfTestTargetParams.fromEnv()); + final CloudFoundryBootDashModel model = harness.getCfTargetModel(); + + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + + List bindServices = ImmutableList.of( + services.createTestService(), services.createTestService() + ); + ACondition.waitFor("services exist "+bindServices, 30_000, () -> { + Set services = client.getServices().stream() + .map(CFServiceInstance::getName) + .collect(Collectors.toSet()); + System.out.println("services = "+services); + assertTrue(services.containsAll(bindServices)); + }); + + final String appName = appHarness.randomAppName(); + + harness.answerDeploymentPrompt(ui(), appName, appName, bindServices); + + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + new ACondition("wait for app '"+ appName +"'to be RUNNING", APP_DEPLOY_TIMEOUT) { + public boolean test() throws Exception { + CloudAppDashElement element = model.getApplication(appName); + assertEquals(RunState.RUNNING, element.getRunState()); + return true; + } + }; + + Set actualServices = client.getBoundServicesSet(appName).block(); + + assertEquals(ImmutableSet.copyOf(bindServices), actualServices); + } + + @Test public void testDeployManifestWithAbsolutePathAttribute() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + IProject project = projects.createProject("to-deploy"); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + waitForJobsToComplete(); + + File zipFile = getTestZip("testapp"); + final String appName = appHarness.randomAppName(); + IFile manifestFile = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " path: "+zipFile.getAbsolutePath() + "\n" + + " buildpack: staticfile_buildpack\n" + ); + harness.answerDeploymentPrompt(ui(), manifestFile); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + ACondition.waitFor("app to appear", APP_IS_VISIBLE_TIMEOUT, () -> { + assertNotNull(model.getApplication(appName)); + }); + + CloudAppDashElement app = model.getApplication(appName); + + ACondition.waitFor("app to be running", APP_DEPLOY_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, app.getRunState()); + String url = pathJoin(app.getUrl(),"test.txt"); + assertEquals(url, "some content here\n", IOUtils.toString(new URL(url))); + }); + + verify(ui()).promptApplicationDeploymentProperties(any()); + verifyNoMoreInteractions(ui()); + } + + @Test public void randomRoute() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForJobsToComplete(); + + final String appName = appHarness.randomAppName(); + IFile manifestFile = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " random-route: true\n" + + " buildpack: java_buildpack\n" + + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ \"jre\": { version: 11.+ } }'\n" + ); + harness.answerDeploymentPrompt(ui(), manifestFile); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + ACondition.waitFor("app to appear", APP_IS_VISIBLE_TIMEOUT, () -> { + assertNotNull(model.getApplication(appName)); + }); + + CloudAppDashElement app = model.getApplication(appName); + + ACondition.waitFor("app to be running", APP_DEPLOY_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, app.getRunState()); + }); + + String host = app.getLiveHost(); + assertTrue("app has host", StringUtil.hasText(host)); + assertTrue("app has default domain", host.endsWith("."+CFAPPS_IO())); + host = host.substring(0, host.length()-CFAPPS_IO().length()-1); + assertTrue("host is random generated on push", !host.equals(appName)); + } + + private String CFAPPS_IO() { + return CloudFoundryClientTest.get_CFAPPS_IO(clientParams); + } + + @Test public void randomRouteWithDomain() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForJobsToComplete(); + + final String appName = appHarness.randomAppName(); + IFile manifestFile = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " domain: "+CFAPPS_IO() + "\n" + + " random-route: true\n" + + " buildpack: java_buildpack\n" + + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ \"jre\": { version: 11.+ } }'\n" + ); + harness.answerDeploymentPrompt(ui(), manifestFile); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + ACondition.waitFor("app to appear", APP_IS_VISIBLE_TIMEOUT, () -> { + assertNotNull(model.getApplication(appName)); + }); + + CloudAppDashElement app = model.getApplication(appName); + + ACondition.waitFor("app to be running", APP_DEPLOY_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, app.getRunState()); + }); + + String host = app.getLiveHost(); + assertTrue("app has host", StringUtil.hasText(host)); + assertTrue("app has default domain", host.endsWith("."+CFAPPS_IO())); + host = host.substring(0, host.length()-CFAPPS_IO().length()-1); + assertTrue("host is random generated on push", !host.equals(appName)); + } + + @Test public void httpRoute() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForJobsToComplete(); + + final String appName = appHarness.randomAppName(); + IFile manifestFile = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " buildpack: java_buildpack\n" + + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ \"jre\": { version: 11.+ } }'\n" + + " routes:\n" + + " - route: " + appName + '.'+CFAPPS_IO() + + ); + harness.answerDeploymentPrompt(ui(), manifestFile); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + ACondition.waitFor("app to appear", APP_IS_VISIBLE_TIMEOUT, () -> { + assertNotNull(model.getApplication(appName)); + }); + + CloudAppDashElement app = model.getApplication(appName); + + ACondition.waitFor("app to be running", APP_DEPLOY_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, app.getRunState()); + }); + + String host = app.getLiveHost(); + assertEquals(appName + '.'+CFAPPS_IO(), host); + } + + @Ignore + @Test public void httpRouteWithPath() throws Exception { + // This fails because the path part is not obtained from element. Bug already raised + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForJobsToComplete(); + + final String appName = appHarness.randomAppName(); + IFile manifestFile = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " buildpack: java_buildpack\n" + + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ \"jre\": { version: 11.+ } }'\n" + + " routes:\n" + + " - route: " + appName + '.'+CFAPPS_IO() + "/myapppath" + + ); + harness.answerDeploymentPrompt(ui(), manifestFile); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + ACondition.waitFor("app to appear", APP_IS_VISIBLE_TIMEOUT, () -> { + assertNotNull(model.getApplication(appName)); + }); + + CloudAppDashElement app = model.getApplication(appName); + + ACondition.waitFor("app to be running", APP_DEPLOY_TIMEOUT, () -> { + assertEquals(RunState.RUNNING, app.getRunState()); + }); + + String host = app.getLiveHost(); + assertEquals(appName + '.'+CFAPPS_IO()+ "/myapppath", host); + } + + private String pathJoin(String url, String append) { + while (url.endsWith("/")) { + url = url.substring(0, url.length()-1); + } + while (append.startsWith("/")) { + append = append.substring(1); + } + return url+"/"+append; + } + + @Test public void deleteService() throws Exception { + String serviceName = services.createTestService(); + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + ACondition.waitFor("service to appear", APP_IS_VISIBLE_TIMEOUT, () -> { + assertNotNull(model.getService(serviceName)); + }); + + when(ui().confirmOperation(contains("Deleting"), contains("Are you sure that you want to delete"))) + .thenReturn(true); + + CloudServiceInstanceDashElement service = model.getService(serviceName); + model.canDelete(service); + model.delete(ImmutableSet.of(service), ui()); + + ACondition.waitFor("service to disapear", SERVICE_DELETE_TIMEOUT, () -> { + assertNull(model.getService(serviceName)); + }); + + } + + /////////////////////////////////////////////////////////////////////////////////// + + private File getTestZip(String fileName) { + File sourceWorkspace = new File( + StsTestUtil.getSourceWorkspacePath("org.springframework.ide.eclipse.boot.dash.test")); + File file = new File(sourceWorkspace, fileName + ".zip"); + Assert.isTrue(file.exists(), ""+ file); + return file; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryBootDashModelMockingTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryBootDashModelMockingTest.java new file mode 100644 index 000000000..f56af1930 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryBootDashModelMockingTest.java @@ -0,0 +1,3143 @@ +/******************************************************************************* + * 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.test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.ide.eclipse.boot.dash.test.BootDashModelTest.waitForJobsToComplete; +import static org.springframework.ide.eclipse.boot.dash.test.CloudFoundryTestHarness.APP_DELETE_TIMEOUT; +import static org.springframework.ide.eclipse.boot.dash.test.CloudFoundryTestHarness.APP_DEPLOY_TIMEOUT; +import static org.springframework.ide.eclipse.boot.dash.test.util.JobUtil.runInJob; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.withPackaging; +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.withStarters; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.assertContains; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.createFile; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.RandomStringUtils; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Platform; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.jface.action.IAction; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.ide.eclipse.boot.core.SpringBootCore; +import org.springframework.ide.eclipse.boot.core.internal.MavenSpringBootProject; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.cf.actions.EnableJmxSshTunnelAction; +import org.springframework.ide.eclipse.boot.dash.cf.actions.RestartApplicationOnlyAction; +import org.springframework.ide.eclipse.boot.dash.cf.actions.SelectManifestAction; +import org.springframework.ide.eclipse.boot.dash.cf.actions.UpdatePasswordAction; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFClientParams; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials.CFCredentialType; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials.LoginMethod; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFDomainType; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.HealthChecks; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFDomainStatus; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.ReactorUtils; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.PasswordDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.StoreCredentialsMode; +import org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.JmxSshTunnelManager; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTargetType; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryTargetProperties; +import org.springframework.ide.eclipse.boot.dash.dialogs.EditTemplateDialogModel; +import org.springframework.ide.eclipse.boot.dash.dialogs.ManifestDiffDialogModel; +import org.springframework.ide.eclipse.boot.dash.model.AbstractBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ModelStateListener; +import org.springframework.ide.eclipse.boot.dash.model.LocalBootDashModel; +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.RunTargetPropertiesManager; +import org.springframework.ide.eclipse.boot.dash.model.SecuredCredentialsStore; +import org.springframework.ide.eclipse.boot.dash.test.CloudFoundryTestHarness.DeploymentAnswerer; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockCFApplication; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockCFSpace; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockCloudFoundryClientFactory; +import org.springframework.ide.eclipse.boot.dash.test.mocks.RunStateHistory; +import org.springframework.ide.eclipse.boot.dash.test.util.ZipDiff; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens; +import org.springframework.ide.eclipse.boot.dash.views.BootDashActions; +import org.springframework.ide.eclipse.boot.dash.views.CustmomizeTargetLabelAction; +import org.springframework.ide.eclipse.boot.dash.views.CustomizeTargetLabelDialogModel; +import org.springframework.ide.eclipse.boot.dash.views.ToggleBootDashModelConnection; +import org.springframework.ide.eclipse.boot.test.AutobuildingEnablement; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; +import org.springframework.ide.eclipse.boot.test.util.TestBracketter; +import org.springsource.ide.eclipse.commons.boot.ls.remoteapps.RemoteBootAppsDataHolder.RemoteAppData; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.frameworks.core.util.ArrayEncoder; +import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil; +import org.springsource.ide.eclipse.commons.frameworks.core.util.StringUtils; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +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.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; +import com.google.gson.Gson; +import com.google.gson.JsonElement; + +/** + * @author Kris De Volder + */ +public class CloudFoundryBootDashModelMockingTest { + + private TestBootDashModelContext context; + private BootProjectTestHarness projects; + private MockCloudFoundryClientFactory clientFactory; + private CloudFoundryTestHarness harness; + private BootDashActions actions; + + //////////////////////////////////////////////////////////// + + public CloudFoundryApplicationHarness appHarness = new CloudFoundryApplicationHarness(null); + + @Rule + public AutobuildingEnablement disableAutoBuild = new AutobuildingEnablement(false); + + @Rule + public TestBracketter testBracketter = new TestBracketter(); + + private SpringBootCore springBootCore = SpringBootCore.getDefault(); + + @Before + public void setup() throws Exception { + System.out.println("java.version="+System.getProperty("java.version")); + StsTestUtil.deleteAllProjects(); + this.clientFactory = new MockCloudFoundryClientFactory(); + this.context = new TestBootDashModelContext( + ResourcesPlugin.getWorkspace(), + DebugPlugin.getDefault().getLaunchManager() + ).withCfClient(clientFactory); + this.harness = CloudFoundryTestHarness.create(context); + this.projects = new BootProjectTestHarness(context.getWorkspace()); + this.actions = new BootDashActions(harness.model, harness.selection.forReading(), harness.sectionSelection, context.injections, null); + } + + @After + public void tearDown() throws Exception { + waitForJobsToComplete(); + appHarness.dispose(); + clientFactory.assertOnlyImplementedStubsCalled(); + harness.dispose(); + } + + //////////////////////////////////////////////////////////// + + @Test + public void testCreateCfTarget() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel target = harness.createCfTarget(targetParams, StoreCredentialsMode.STORE_PASSWORD); + + assertNotNull(target); + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertEquals(CFCredentialType.PASSWORD, credentials.getType()); + assertNotNull(credentials.getSecret()); + assertEquals(1, harness.getCfRunTargetModels().size()); + } + + @Test public void refreshTokenStreamTerminatedOnDispose() throws Exception { + CFClientParams params = CfTestTargetParams.fromEnv(); + clientFactory.defSpace(params.getOrgName(), params.getSpaceName()); + ClientRequests client = clientFactory.getClient(params); + + CountDownLatch latch = new CountDownLatch(1); + Future> tokens = runInJob(() -> { + return client.getRefreshTokens() + .doOnNext(token -> latch.countDown()) + .collect(Collectors.toList()).block(Duration.ofSeconds(10)); + }); + client.getApplicationsWithBasicInfo(); // forces the client to authenticate + latch.await(); //To avoid race condition (job tends to be slow and might start asking tokens only after disposed client (and this yields a empty stream of tokens) + client.dispose(); + assertEquals(ImmutableList.of(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN), tokens.get()); + } + + + @Test + public void testCreateCfTargetSsoAndStoreRefreshToken() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnvWithCredentials(CFCredentials.fromSsoToken(clientFactory.getSsoToken())); + { + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel target = harness.createCfTarget(targetParams, StoreCredentialsMode.STORE_TOKEN); + + assertNotNull(target); + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertEquals(CFCredentialType.REFRESH_TOKEN, credentials.getType()); + assertEquals(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN, credentials.getSecret()); + assertEquals(1, harness.getCfRunTargetModels().size()); + } + harness.reload(); + + CloudFoundryBootDashModel target = harness.getCfTargetModel(); + assertNotNull(target); + + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertEquals(CFCredentialType.REFRESH_TOKEN, credentials.getType()); + assertEquals(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN, credentials.getSecret()); + assertEquals(StoreCredentialsMode.STORE_TOKEN, target.getRunTarget().getTargetProperties().getStoreCredentials()); + + waitForJobsToComplete(); + assertTrue(target.isConnected()); //should auto connect. + verifyZeroInteractions(ui()); //should not prompt for password (but used stored pass). + + { + SecuredCredentialsStore store = harness.getCredentialsStore(); + assertFalse(store.isUnlocked()); //not unlocked because token is stored in simple private file + } + + { + String storedCred = getStoredToken(target); + assertEquals(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN, storedCred); + } + + clientFactory.changeRefrestToken("another-1"); + clientFactory.changeRefrestToken("another-2"); + ACondition.waitFor("changed stored token", 300, () -> { + assertEquals("another-2", getStoredToken(target)); + }); + } + + private AllUserInteractions ui() { + return context.injections.getBean(AllUserInteractions.class); + } + + private String getStoredToken(CloudFoundryBootDashModel target) { + IPropertyStore store = harness.getPrivateStore(); + String key = harness.privateStoreKey(target); + String storedCred = store.get(key); + return storedCred; + } + + @Test + public void testCreateCfTargetSsoAndStoreNothing() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnvWithCredentials(CFCredentials.fromSsoToken(clientFactory.getSsoToken())); + { + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel target = harness.createCfTarget(targetParams, StoreCredentialsMode.STORE_NOTHING); + + assertNotNull(target); + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertEquals(CFCredentialType.REFRESH_TOKEN, credentials.getType()); + assertEquals(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN, credentials.getSecret()); + assertEquals(1, harness.getCfRunTargetModels().size()); + } + harness.reload(); + + CloudFoundryBootDashModel target = harness.getCfTargetModel(); + assertNotNull(target); + + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertNull(credentials); + + waitForJobsToComplete(); + assertFalse(target.isConnected()); //should not auto connect. + verifyZeroInteractions(ui()); + + { + SecuredCredentialsStore store = harness.getCredentialsStore(); + assertFalse(store.isUnlocked()); //not unlocked because token is stored in simple private file + assertNull(store.getCredentials(harness.secureStoreKey(target))); + } + + { + IPropertyStore store = harness.getPrivateStore(); + String key = harness.privateStoreKey(target); + String storedCred = store.get(key); + assertNull(storedCred); + } + } + + @Test + public void testCreateCfTargetSsoAndStorePassword() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnvWithCredentials(CFCredentials.fromSsoToken(clientFactory.getSsoToken())); + { + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel target = harness.createCfTarget(targetParams, StoreCredentialsMode.STORE_PASSWORD, (wizard) -> { + assertContains("'Store Password' is useless for a 'Temporary Code'", wizard.getValidator().getValue().msg); + }); + //STORE_PASSWORD is meaningless for sso login and should be ignored (so behaves as STORE_NOTHING instead) + + assertNotNull(target); + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertEquals(CFCredentialType.REFRESH_TOKEN, credentials.getType()); + assertEquals(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN, credentials.getSecret()); + assertEquals(1, harness.getCfRunTargetModels().size()); + } + harness.reload(); + + CloudFoundryBootDashModel target = harness.getCfTargetModel(); + assertNotNull(target); + + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertNull(credentials); + + waitForJobsToComplete(); + assertFalse(target.isConnected()); //should not auto connect. + verifyZeroInteractions(ui()); + + { + SecuredCredentialsStore store = harness.getCredentialsStore(); + assertFalse(store.isUnlocked()); //not unlocked because token is stored in simple private file + assertNull(store.getCredentials(harness.secureStoreKey(target))); + } + + { + IPropertyStore store = harness.getPrivateStore(); + String key = harness.privateStoreKey(target); + String storedCred = store.get(key); + assertNull(storedCred); + } + } + + @Test public void testCreateCfTargetAndStorePassword() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + { + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel target = harness.createCfTarget(targetParams, StoreCredentialsMode.STORE_PASSWORD); + + assertNotNull(target); + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertEquals(CFCredentialType.PASSWORD, credentials.getType()); + assertNotNull(credentials.getSecret()); + assertEquals(1, harness.getCfRunTargetModels().size()); + + SecuredCredentialsStore store = harness.getCredentialsStore(); + assertTrue(store.isUnlocked()); + } + + harness.reload(); + { + CloudFoundryBootDashModel target = harness.getCfTargetModel(); + String expectedPass = targetParams.getCredentials().getSecret(); + + assertNotNull(target); + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + String password = credentials.getSecret(); + assertEquals(CFCredentialType.PASSWORD, credentials.getType()); + assertEquals(expectedPass, password); + assertEquals(StoreCredentialsMode.STORE_PASSWORD, target.getRunTarget().getTargetProperties().getStoreCredentials()); + + waitForJobsToComplete(); + assertTrue(target.isConnected()); //should auto connect. + verifyZeroInteractions(ui()); //should not prompt for password (but used stored pass). + + { + SecuredCredentialsStore store = harness.getCredentialsStore(); + assertTrue(store.isUnlocked()); + String key = harness.secureStoreKey(target); + String storedCred = store.getCredentials(key); + assertEquals(expectedPass, storedCred); + } + + { + IPropertyStore store = harness.getPrivateStore(); + String key = harness.privateStoreKey(target); + String storedCred = store.get(key); + assertNull(storedCred); + } + } + } + + @Test public void testCreateCfTargetAndForgetPassword() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + { + CloudFoundryBootDashModel target = harness.createCfTarget(targetParams, StoreCredentialsMode.STORE_NOTHING); + + assertNotNull(target); + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertEquals(CFCredentialType.REFRESH_TOKEN, credentials.getType()); + assertNotNull(credentials.getSecret()); + assertEquals(1, harness.getCfRunTargetModels().size()); + + SecuredCredentialsStore store = harness.getCredentialsStore(); + assertFalse(store.isUnlocked()); // should not have gotten unlocked. + } + + harness.reload(); + + { + CloudFoundryBootDashModel target = harness.getCfTargetModel(); + + waitForJobsToComplete(); + + assertEquals(StoreCredentialsMode.STORE_NOTHING, target.getRunTarget().getTargetProperties().getStoreCredentials()); + assertNotNull(target); + assertNull(target.getRunTarget().getTargetProperties().getCredentials()); + assertFalse(target.isConnected()); // no auto connect if no creds are stored. + { //check secure store is clean + SecuredCredentialsStore store = harness.getCredentialsStore(); + assertFalse(store.isUnlocked()); // should not have gotten unlocked. + String storedCred = store.getCredentials(harness.secureStoreKey(target)); + assertNull(storedCred); + } + { //check private store is clean + IPropertyStore store = harness.getPrivateStore(); + String storedCred = store.get(harness.privateStoreKey(target)); + assertNull(storedCred); + } + + verifyZeroInteractions(ui()); + + //When we connect... the user should get prompted for password + harness.answerPasswordPrompt(ui(), (d) -> { + d.getMethodVar().setValue(targetParams.getCredentials().getType().toLoginMethod()); + d.getPasswordVar().setValue(targetParams.getCredentials().getSecret()); + d.validateCredentials().block(); + d.performOk(); + }); + + harness.sectionSelection.setValue(target); + IAction connectAction = toggleTargetConnectionAction(); + connectAction.run(); + + ACondition.waitFor("connected to target", 5_000, () -> { + assertTrue(target.isConnected()); + }); + + verify(ui()).openPasswordDialog(any()); + verifyNoMoreInteractions(ui()); + } + } + + @Test public void testCreateCfTargetAndStoreToken() throws Exception { + + { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + space.defApp("foo"); + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + CloudFoundryBootDashModel target = harness.createCfTarget(targetParams, StoreCredentialsMode.STORE_TOKEN); + assertNotNull(target); + assertTrue(target.isConnected()); + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertEquals(CFCredentialType.REFRESH_TOKEN, credentials.getType()); + assertEquals(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN, credentials.getSecret()); + assertEquals(1, harness.getCfRunTargetModels().size()); + } + + harness.reload(); + + { + CloudFoundryBootDashModel target = harness.getCfTargetModel(); + + { //secure store shouldn't have been accessed (i.e. avoid opening it and popping password) + SecuredCredentialsStore store = harness.getCredentialsStore(); + assertFalse(store.isUnlocked()); + String key = harness.secureStoreKey(target); + String storedCred = store.getCredentials(key); + assertNull(storedCred); + } + { + IPropertyStore store = harness.getPrivateStore(); + String key = harness.privateStoreKey(target); + String storedCred = store.get(key); + assertEquals(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN, storedCred); + } + + assertNotNull(target); + CFCredentials credentials = target.getRunTarget().getTargetProperties().getCredentials(); + assertEquals(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN, credentials.getSecret()); + assertEquals(CFCredentialType.REFRESH_TOKEN, credentials.getType()); + assertEquals(StoreCredentialsMode.STORE_TOKEN, target.getRunTarget().getTargetProperties().getStoreCredentials()); + + waitForJobsToComplete(); + assertTrue(target.isConnected()); //should auto connect. + verifyZeroInteractions(ui()); //should not prompt for password (but used stored token). + + clientFactory.changeRefrestToken("another-1"); + clientFactory.changeRefrestToken("another-2"); + ACondition.waitFor("changed stored token", 300, () -> { + assertEquals("another-2", getStoredToken(target)); + }); + } + } + + @Test + public void testAppsShownInBootDash() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + space.defApp("foo"); + space.defApp("bar"); + + final CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + + new ACondition("wait for apps to appear", 3000) { + @Override + public boolean test() throws Exception { + ImmutableSet appNames = getNames(target.getApplications().getValues()); + assertEquals(ImmutableSet.of("foo", "bar"), appNames); + return true; + } + }; + } + + @Test + public void testBasicRefreshApps() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + space.defApp("foo"); + space.defApp("bar"); + + final CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + + waitForApps(target, "foo", "bar"); + + space.defApp("anotherfoo"); + space.defApp("anotherbar"); + target.refresh(ui()); + + waitForApps(target, "foo", "bar", "anotherfoo", "anotherbar"); + } + + @Test + public void testRefreshAppsRunState() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + final MockCFApplication foo = space.defApp("foo"); + space.defApp("bar"); + + final CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + + waitForApps(target, "foo", "bar"); + + foo.start(CancelationTokens.NULL); + + target.refresh(ui()); + + new ACondition("wait for app states", 3000) { + @Override + public boolean test() throws Exception { + ImmutableSet appNames = getNames(target.getApplications().getValues()); + assertEquals(ImmutableSet.of("foo", "bar"), appNames); + CloudAppDashElement appElement = harness.getCfTargetModel().getApplication("foo"); + assertEquals(RunState.RUNNING, appElement.getRunState()); + + appElement = harness.getCfTargetModel().getApplication("bar"); + assertEquals(RunState.INACTIVE, appElement.getRunState()); + + return true; + } + }; + } + + @Test + public void testRefreshAppsHealthCheck() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + final MockCFApplication foo = space.defApp("foo"); + + + final CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + + waitForApps(target, "foo"); + + CloudAppDashElement appElement = harness.getCfTargetModel().getApplication("foo"); + assertEquals(HealthChecks.HC_PORT, appElement.getHealthCheck()); + + + foo.setHealthCheckType(HealthChecks.HC_PROCESS); + + target.refresh(ui()); + + new ACondition("wait for app health check", 3000) { + @Override + public boolean test() throws Exception { + ImmutableSet appNames = getNames(target.getApplications().getValues()); + assertEquals(ImmutableSet.of("foo"), appNames); + + CloudAppDashElement appElement = harness.getCfTargetModel().getApplication("foo"); + assertEquals(HealthChecks.HC_PROCESS, appElement.getHealthCheck()); + + return true; + } + }; + } + + @Test + public void testRefreshServices() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + space.defApp("foo"); + space.defService("elephantsql"); + space.defService("cleardb"); + + final CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + + waitForApps(target, "foo"); + waitForServices(target, "elephantsql", "cleardb"); + waitForElements(target, "foo", "elephantsql", "cleardb"); + + space.defService("rabbit"); + + target.refresh(ui()); + waitForServices(target, "elephantsql", "cleardb", "rabbit"); + waitForElements(target, "foo", "elephantsql", "cleardb", "rabbit"); + } + + @Test + public void testAppsAndServicesShownInBootDash() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + space.defApp("foo"); + space.defApp("bar"); + space.defService("a-sql"); + space.defService("z-rabbit"); + + final CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + assertTrue(target.isConnected()); + + debugListener("applications", target.getApplications()); + debugListener("services", target.getServices()); + debugListener("all", target.getElements()); + + new ACondition("wait for elements to appear", 3000) { + @Override + public boolean test() throws Exception { + ImmutableSet appNames = getNames(target.getApplications().getValues()); + ImmutableSet serviceNames = getNames(target.getServices().getValues()); + ImmutableSet allNames = getNames(target.getElements().getValues()); + assertEquals(ImmutableSet.of("foo", "bar"), appNames); + assertEquals(ImmutableSet.of("a-sql", "z-rabbit"), serviceNames); + assertEquals(ImmutableSet.of("foo", "bar", "a-sql", "z-rabbit"), allNames); + return true; + } + }; + + //Also test we sort this stuff in the right order. + + ArrayList elements = new ArrayList<>(target.getElements().getValues()); + Collections.sort(elements, target.getElementComparator()); + assertNames(elements, + //first apps... alphabetic + "bar", + "foo", + //then services... alphabetic + "a-sql", + "z-rabbit" + ); + + //For https://www.pivotaltracker.com/story/show/114408475 + // Apps and services should disappear when target is disconnected + + IAction toggleConnection = toggleTargetConnectionAction(); + harness.sectionSelection.setValue(target); + toggleConnection.run(); + + new ACondition("wait for elements to disappear", 10000) { + @Override + public boolean test() throws Exception { + assertFalse(target.isConnected()); + ImmutableSet appNames = getNames(target.getApplications().getValues()); + ImmutableSet serviceNames = getNames(target.getServices().getValues()); + ImmutableSet allNames = getNames(target.getElements().getValues()); + assertEquals(ImmutableSet.of(), appNames); + assertEquals(ImmutableSet.of(), serviceNames); + assertEquals(ImmutableSet.of(), allNames); + return true; + } + }; + } + + + @Test + public void testDeployActionsSorted() throws Exception { + //Generate some random 'space' names. + String orgName = "CloudRunAMock"; + String[] spaceNames = new String[6]; + for (int i = 0; i < spaceNames.length; i++) { + spaceNames[i] = RandomStringUtils.randomAlphabetic(10).toLowerCase(); + } + + //Define the spaces in the 'mock' cloud: + for (String spaceName : spaceNames) { + //Since this is just a mock client we creating, the params don't matter all that much at all. + clientFactory.defSpace(orgName, spaceName); + } + + //Create targets in the boot dash that connect to these spaces: + for (String spaceName : spaceNames) { + CFClientParams params = new CFClientParams( + "https://api.run.cloud.mock.com", + "some-user", CFCredentials.fromLogin(LoginMethod.PASSWORD, MockCloudFoundryClientFactory.FAKE_PASSWORD), + false, + orgName, + spaceName, + false + ); + harness.createCfTarget(params); + } + + { + ImmutableList deployActions = actions.getDebugOnTargetActions(); + assertEquals(spaceNames.length, deployActions.size()); + assertSorted(deployActions); + } + + { + ImmutableList deployActions = actions.getRunOnTargetActions(); + assertEquals(spaceNames.length, deployActions.size()); + assertSorted(deployActions); + } + + } + + @Test + public void targetTypeProperties() throws Exception { + { + CloudFoundryRunTargetType cfTargetType = harness.getCfTargetType(); + PropertyStoreApi props = cfTargetType.getPersistentProperties(); + props.put("testkey", "testvalue"); + assertEquals("testvalue", props.get("testkey")); + } + + harness.reload(); + + { + CloudFoundryRunTargetType cfTargetType = harness.getCfTargetType(); + PropertyStoreApi props = cfTargetType.getPersistentProperties(); + assertEquals("testvalue", props.get("testkey")); + } + + } + + @Test + public void appsManagerDefaultHost() throws Exception { + MockCFSpace space = clientFactory.defSpace("my-org", "foo"); + + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + + CloudFoundryBootDashModel cfModel = harness.createCfTarget(new CFClientParams( + apiUrl, + username, CFCredentials.fromLogin(LoginMethod.PASSWORD, password), + false, + "my-org", + "foo", + false)); + + assertEquals("https://console.some-cloud.com", cfModel.getRunTarget().getAppsManagerHost()); + assertEquals("https://console.some-cloud.com", cfModel.getRunTarget().getAppsManagerHostDefault()); + + assertEquals("https://console.some-cloud.com/organizations/" + space.getOrganization().getGuid() + "/spaces/" + space.getGuid(), cfModel.getRunTarget().getAppsManagerURL()); + } + + @Test + public void appsManagerCustomizedHost() throws Exception { + MockCFSpace space = clientFactory.defSpace("my-org", "foo"); + + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + + CloudFoundryBootDashModel cfModel = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "foo", false + )); + + cfModel.getRunTarget().setAppsManagerHost("https://totallyDifferentHost.com"); + + assertEquals("https://totallyDifferentHost.com", cfModel.getRunTarget().getAppsManagerHost()); + assertEquals("https://console.some-cloud.com", cfModel.getRunTarget().getAppsManagerHostDefault()); + + assertEquals("https://totallyDifferentHost.com/organizations/" + space.getOrganization().getGuid() + "/spaces/" + space.getGuid(), cfModel.getRunTarget().getAppsManagerURL()); + } + + @Test + public void templateDrivenTargetNames() throws Exception { + clientFactory.defSpace("my-org", "foo"); + clientFactory.defSpace("your-org", "bar"); + + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + AbstractBootDashModel fooSpace = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "foo", false)); + AbstractBootDashModel barSpace = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "your-org", "bar", false)); + + //check the default rendering is like it used to be before introducing templates. + assertEquals("my-org : foo - [https://api.some-cloud.com]", fooSpace.getDisplayName()); + assertEquals("your-org : bar - [https://api.some-cloud.com]", barSpace.getDisplayName()); + + RunTargetType targetType = fooSpace.getRunTarget().getType(); + + //Let's try switching the order of org and space + targetType.setNameTemplate("%s - %o @ %a"); + assertEquals("foo - my-org @ https://api.some-cloud.com", fooSpace.getDisplayName()); + assertEquals("bar - your-org @ https://api.some-cloud.com", barSpace.getDisplayName()); + + //Let's try adding 'username' into the label + targetType.setNameTemplate("%u@%s"); + assertEquals("freddy@foo", fooSpace.getDisplayName()); + assertEquals("freddy@bar", barSpace.getDisplayName()); + } + + @Test public void pushTcpRouteWithRandomPort() throws Exception { + String appName = "moriarty-app"; + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + + MockCFSpace space = clientFactory.defSpace("my-org", "my-space"); + clientFactory.defDomain("tcp.domain.com", CFDomainType.TCP, CFDomainStatus.SHARED); + + assertEquals("cfmockapps.io", clientFactory.getDefaultDomain()); + + CloudFoundryBootDashModel target = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "my-space", false)); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + IFile manifest = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " random-route: true\n" + + " domain: tcp.domain.com\n" + + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 11.+}}'\n" + ); + harness.answerDeploymentPrompt(ui(), manifest); + target.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(target, appName); + CloudAppDashElement app = getApplication(target, project); + waitForState(app, RunState.RUNNING, 10_000); + + MockCFApplication deployedApp = space.getApplication(appName); + List uris = deployedApp.getBasicInfo().getUris(); + assertEquals(ImmutableList.of("tcp.domain.com:63000"), uris); + + doUnchangedAppRestartTest(app, deployedApp); + } + + @Test public void redeploy_app_and_enable_ssh_tunnel() throws Exception { + String appName = "tunneled-jmx-app"; + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + + MockCFSpace space = clientFactory.defSpace("my-org", "my-space"); + CloudFoundryBootDashModel target = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "my-space", false)); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + String yaml = "applications:\n" + + "- name: "+appName+"\n"; + IFile manifest = createFile(project, "manifest.yml", yaml); + harness.answerDeploymentPrompt(ui(), new DeploymentAnswerer(yaml) { + @Override + public void apply(CloudApplicationDeploymentProperties properties) throws Exception { + properties.setManifestFile(manifest); + properties.setEnableJmxSshTunnel(false); + } + }); + target.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(target, appName); + CloudAppDashElement app = getApplication(target, project); + waitForState(app, RunState.RUNNING, 10_000); + assertFalse(app.getEnableJmxSshTunnel()); + + ACondition.waitFor("stop hammering", 20000, () -> { + app.stopAsync(); + assertEquals(RunState.INACTIVE, app.getRunState()); + }); + + //Now... redeploy and overwrite, cahning ssh enablement + reset(ui()); + harness.answerDeploymentPrompt(ui(), new DeploymentAnswerer(yaml) { + @Override + public void apply(CloudApplicationDeploymentProperties properties) throws Exception { + properties.setManifestFile(manifest); + properties.setEnableJmxSshTunnel(true); + } + }); + + Mockito.doReturn(ManifestDiffDialogModel.Result.USE_MANIFEST).when(ui()).confirmReplaceApp(any(), any(), any(), any()); + target.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + ACondition.waitFor("app restart", 20000, () -> { + assertEquals(RunState.RUNNING, app.getRunState()); + }); + JmxSshTunnelManager tunnels = harness.getJmxSshTunnelManager(); + ACondition.waitFor("sshtunnel creation", 2_000, () -> { + int jmxPort = app.getCfJmxPort(); + assertEquals( + ImmutableSet.of(remoteAppData(ImmutableMap.of( + "jmxurl", "service:jmx:rmi://localhost:"+jmxPort+"/jndi/rmi://localhost:"+jmxPort+"/jmxrmi", + "host", "tunneled-jmx-app.cfmockapps.io", + "keepChecking", false, + "processId", app.getAppGuid().toString() + ))), + tunnels.getUrls().getValues() + ); + }); + + ACondition.waitFor("stop hammering", 20000, () -> { + app.stopAsync(); + assertEquals(RunState.INACTIVE, app.getRunState()); + }); + ACondition.waitFor("tunnel closed", 2_000, () -> { + assertEquals(ImmutableSet.of(), tunnels.getUrls().getValues()); + }); + } + + @Test public void enable_jmx_after_deploy_decline_restart() throws Exception { + //See: https://www.pivotaltracker.com/story/show/159349926 + String appName = "tunneled-jmx-app"; + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + + MockCFSpace space = clientFactory.defSpace("my-org", "my-space"); + CloudFoundryBootDashModel target = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "my-space", false)); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + String yaml = "applications:\n" + + "- name: "+appName+"\n"; + IFile manifest = createFile(project, "manifest.yml", yaml); + harness.answerDeploymentPrompt(ui(), new DeploymentAnswerer(yaml) { + @Override + public void apply(CloudApplicationDeploymentProperties properties) throws Exception { + properties.setManifestFile(manifest); + properties.setEnableJmxSshTunnel(false); + } + }); + target.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(target, appName); + CloudAppDashElement app = getApplication(target, project); + waitForState(app, RunState.RUNNING, 10_000); + assertFalse(app.getEnableJmxSshTunnel()); + + harness.selection.setElements(app); + EnableJmxSshTunnelAction toggleJmx = enableJmxSshTunnel(); + assertTrue(toggleJmx.isEnabled()); + assertTrue(toggleJmx.isVisible()); + assertEquals("Enable JMX Ssh Tunnelling", toggleJmx.getText()); + + harness.answerConfirmationMultipleChoice(ui(), (title, msg, choices, defaultIndex) -> { + assertEquals("Enabling JMX Requires Restart", title); + for (int i = 0; i < choices.length; i++) { + if (choices[i].startsWith("No")) { + return i; + } + } + throw new IllegalStateException("Didn't find expected choice"); + }); + + toggleJmx.run(); + assertTrue(app.getEnableJmxSshTunnel()); + + JmxSshTunnelManager tunnels = harness.getJmxSshTunnelManager(); + ACondition.expectAllways("sshtunnel creation", 2_000, () -> { + assertTrue(tunnels.getUrls().getValues().isEmpty()); + }); + + ACondition.waitFor("stop hammering", 2_000, () -> { + app.stopAsync(); + assertEquals(RunState.INACTIVE, app.getRunState()); + }); + } + + @Test public void enable_and_disable_jmx_after_deploy() throws Exception { + //See: https://www.pivotaltracker.com/story/show/159349926 + String appName = "tunneled-jmx-app"; + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + + MockCFSpace space = clientFactory.defSpace("my-org", "my-space"); + CloudFoundryBootDashModel target = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "my-space", false)); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + String yaml = "applications:\n" + + "- name: "+appName+"\n"; + IFile manifest = createFile(project, "manifest.yml", yaml); + harness.answerDeploymentPrompt(ui(), new DeploymentAnswerer(yaml) { + @Override + public void apply(CloudApplicationDeploymentProperties properties) throws Exception { + properties.setEnableJmxSshTunnel(false); + properties.setManifestFile(manifest); + } + }); + target.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(target, appName); + CloudAppDashElement app = getApplication(target, project); + waitForState(app, RunState.RUNNING, 10_000); + assertFalse(app.getEnableJmxSshTunnel()); + + harness.selection.setElements(app); + EnableJmxSshTunnelAction toggleJmx = enableJmxSshTunnel(); + assertTrue(toggleJmx.isEnabled()); + assertTrue(toggleJmx.isVisible()); + assertEquals("Enable JMX Ssh Tunnelling", toggleJmx.getText()); + + harness.answerConfirmationMultipleChoice(ui(), (title, msg, choices, defaultIndex) -> { + assertEquals("Enabling JMX Requires Restart", title); + for (int i = 0; i < choices.length; i++) { + if (choices[i].startsWith("Yes")) { + return i; + } + } + throw new IllegalStateException("Didn't find expected choice"); + }); + + toggleJmx.run(); + assertTrue(app.getEnableJmxSshTunnel()); + + JmxSshTunnelManager tunnels = harness.getJmxSshTunnelManager(); + ACondition.waitFor("sshtunnel creation", 2_000, () -> { + int jmxPort = app.getCfJmxPort(); + assertEquals( + ImmutableSet.of(remoteAppData(ImmutableMap.of( + "jmxurl", "service:jmx:rmi://localhost:"+jmxPort+"/jndi/rmi://localhost:"+jmxPort+"/jmxrmi", + "host", "tunneled-jmx-app.cfmockapps.io", + "keepChecking", false, + "processId", app.getAppGuid().toString() + ))), + tunnels.getUrls().getValues() + ); + }); + + reset(ui()); + harness.answerConfirmationMultipleChoice(ui(), (title, msg, choices, defaultIndex) -> { + assertEquals("Disabling JMX Requires Restart", title); + for (int i = 0; i < choices.length; i++) { + if (choices[i].startsWith("Yes")) { + return i; + } + } + throw new IllegalStateException("Didn't find expected choice"); + }); + + assertTrue(toggleJmx.isEnabled()); + assertTrue(toggleJmx.isVisible()); + assertEquals("Disable JMX Ssh Tunnelling", toggleJmx.getText()); + + toggleJmx.run(); + assertFalse(app.getEnableJmxSshTunnel()); + + ACondition.waitFor("tunnel closed", 2_000, () -> { + assertEquals(ImmutableSet.of(), tunnels.getUrls().getValues()); + }); + ACondition.waitFor("app restarted", 2_000, () -> { + assertEquals(RunState.RUNNING, app.getRunState()); + }); + ACondition.expectAllways("sshtunnel creation", 2_000, () -> { + assertTrue(tunnels.getUrls().getValues().isEmpty()); + }); + + ACondition.waitFor("stop hammering", 2_000, () -> { + app.stopAsync(); + assertEquals(RunState.INACTIVE, app.getRunState()); + }); + } + + private RemoteAppData remoteAppData(ImmutableMap map) { + Gson gson = new Gson(); + JsonElement tree = gson.toJsonTree(map); + return gson.fromJson(tree, RemoteAppData.class); + } + + @Test public void deploy_app_with_jmx_ssh_tunnel_enabled() throws Exception { + String appName = "tunneled-jmx-app"; + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + + MockCFSpace space = clientFactory.defSpace("my-org", "my-space"); + clientFactory.defDomain("tcp.domain.com", CFDomainType.TCP, CFDomainStatus.SHARED); + + CloudFoundryBootDashModel target = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "my-space", false)); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + final String yaml = "applications:\n" + + "- name: "+appName+"\n"; + IFile manifest = createFile(project, "manifest.yml", yaml); + harness.answerDeploymentPrompt(ui(), new DeploymentAnswerer(yaml) { + @Override + public void apply(CloudApplicationDeploymentProperties properties) throws Exception { + properties.setManifestFile(manifest); + properties.setEnableJmxSshTunnel(true); + } + }); + target.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(target, appName); + CloudAppDashElement app = getApplication(target, project); + waitForState(app, RunState.RUNNING, 10_000); + + assertTrue(app.getEnableJmxSshTunnel()); + JmxSshTunnelManager tunnels = harness.getJmxSshTunnelManager(); + ACondition.waitFor("sshtunnel creation", 2_000, () -> { + int jmxPort = app.getCfJmxPort(); + assertTrue(jmxPort>0); + assertEquals( + ImmutableSet.of(remoteAppData(ImmutableMap.of( + "jmxurl", "service:jmx:rmi://localhost:"+jmxPort+"/jndi/rmi://localhost:"+jmxPort+"/jmxrmi", + "host", "tunneled-jmx-app.cfmockapps.io", + "keepChecking", false, + "processId", app.getAppGuid().toString() + ))), + tunnels.getUrls().getValues() + ); + }); + + ACondition.waitFor("stop hammering", 20000, () -> { + app.stopAsync(); + assertEquals(RunState.INACTIVE, app.getRunState()); + }); + } + + @Test public void jmx_ssh_tunnel_created_on_eclipse_restart() throws Exception { + String appName = "tunneled-jmx-app"; + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + + { + MockCFSpace space = clientFactory.defSpace("my-org", "my-space"); + clientFactory.defDomain("tcp.domain.com", CFDomainType.TCP, CFDomainStatus.SHARED); + + CloudFoundryBootDashModel target = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "my-space", false)); + String yaml = "applications:\n" + + "- name: "+appName+"\n"; + IFile manifest = createFile(project, "manifest.yml", yaml); + harness.answerDeploymentPrompt(ui(), new DeploymentAnswerer(yaml) { + @Override + public void apply(CloudApplicationDeploymentProperties properties) throws Exception { + properties.setManifestFile(manifest); + properties.setEnableJmxSshTunnel(true); + } + }); + target.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(target, appName); + CloudAppDashElement app = getApplication(target, project); + waitForState(app, RunState.RUNNING, 10_000); + + assertTrue(app.getEnableJmxSshTunnel()); + int jmxPort = app.getCfJmxPort(); + JmxSshTunnelManager tunnels = harness.getJmxSshTunnelManager(); + ACondition.waitFor("sshtunnel creation", 2_000, () -> { + assertEquals( + ImmutableSet.of(remoteAppData(ImmutableMap.of( + "jmxurl", "service:jmx:rmi://localhost:"+jmxPort+"/jndi/rmi://localhost:"+jmxPort+"/jmxrmi", + "host", "tunneled-jmx-app.cfmockapps.io", + "keepChecking", false, + "processId", app.getAppGuid().toString() + ))), + tunnels.getUrls().getValues() + ); + }); + } + + harness.reload(); + + { + CloudFoundryBootDashModel target = harness.getCfTargetModel(); + waitForApps(target, appName); + CloudAppDashElement app = getApplication(target, project); + waitForState(app, RunState.RUNNING, 10_000); + + assertTrue(app.getEnableJmxSshTunnel()); + int jmxPort = app.getCfJmxPort(); + JmxSshTunnelManager tunnels = harness.getJmxSshTunnelManager(); + ACondition.waitFor("sshtunnel creation", 2_000, () -> { + assertEquals( + ImmutableSet.of(remoteAppData(ImmutableMap.of( + "jmxurl", "service:jmx:rmi://localhost:"+jmxPort+"/jndi/rmi://localhost:"+jmxPort+"/jmxrmi", + "host", "tunneled-jmx-app.cfmockapps.io", + "keepChecking", false, + "processId", app.getAppGuid().toString() + ))), + tunnels.getUrls().getValues() + ); + }); + } + + + } + + @Test public void deploy_app_with_jmx_ssh_tunnel_disabled() throws Exception { + String appName = "tunneled-jmx-app"; + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + + MockCFSpace space = clientFactory.defSpace("my-org", "my-space"); + clientFactory.defDomain("tcp.domain.com", CFDomainType.TCP, CFDomainStatus.SHARED); + + CloudFoundryBootDashModel target = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "my-space", false)); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + IFile manifest = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + ); + harness.answerDeploymentPrompt(ui(), manifest); //Note: don't need to disable explictly because its the default. + target.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(target, appName); + CloudAppDashElement app = getApplication(target, project); + waitForState(app, RunState.RUNNING, 10_000); + + assertFalse(app.getEnableJmxSshTunnel()); + assertEquals(-1, app.getCfJmxPort()); + } + + @Test public void pushTcpRouteWithFixedPort() throws Exception { + String appName = "moriarty-app"; + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + + MockCFSpace space = clientFactory.defSpace("my-org", "my-space"); + clientFactory.defDomain("tcp.domain.com", CFDomainType.TCP, CFDomainStatus.SHARED); + + CloudFoundryBootDashModel target = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "my-space", false)); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + IFile manifest = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " routes: \n" + + " - route: tcp.domain.com:61001\n" + + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 11.+}}'\n" + ); + harness.answerDeploymentPrompt(ui(), manifest); + target.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(target, appName); + CloudAppDashElement app = getApplication(target, project); + waitForState(app, RunState.RUNNING, 10_000); + + MockCFApplication deployedApp = space.getApplication(appName); + List uris = deployedApp.getBasicInfo().getUris(); //Note: this info isn't retrieved via client! + assertEquals(ImmutableList.of("tcp.domain.com:61001"), uris); //Note: this info wasn't retrieved via client! Check if client info agrees + assertEquals(uris, target.getClient().getApplication(appName).getUris()); //Note: this info wasn't retrieved via client! Check if client info agrees + + doUnchangedAppRestartTest(app, deployedApp); + } + + @Test public void pushHttpRouteWithRandomRoute() throws Exception { + String appName = "moriarty-app"; + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + + MockCFSpace space = clientFactory.defSpace("my-org", "my-space"); + clientFactory.defDomain("tcp.domain.com", CFDomainType.TCP, CFDomainStatus.SHARED); + + assertEquals("cfmockapps.io", clientFactory.getDefaultDomain()); + + CloudFoundryBootDashModel target = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "my-space", false)); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + IFile manifest = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " random-route: true\n" + + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 11.+}}'\n" + ); + harness.answerDeploymentPrompt(ui(), manifest); + target.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(target, appName); + CloudAppDashElement app = getApplication(target, project); + waitForState(app, RunState.RUNNING, 10_000); + + MockCFApplication deployedApp = space.getApplication(appName); + List uris = deployedApp.getBasicInfo().getUris(); + assertTrue(uris.size() == 1); + assertTrue(uris.get(0).length() > ".cfmockapps.io".length()); + assertTrue(uris.get(0).endsWith(".cfmockapps.io")); + + doUnchangedAppRestartTest(app, deployedApp); + } + + @Test + public void customizeTargetLabelAction() throws Exception { + clientFactory.defSpace("my-org", "foo"); + clientFactory.defSpace("your-org", "bar"); + + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + LocalBootDashModel local = harness.getLocalModel(); + AbstractBootDashModel fooSpace = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "foo", false)); + AbstractBootDashModel barSpace = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "your-org", "bar", false)); + CustmomizeTargetLabelAction action = actions.getCustomizeTargetLabelAction(); + + //////////// not applicable for local targets: + + harness.sectionSelection.setValue(local); + assertFalse(action.isEnabled()); + assertFalse(action.isVisible()); + + //////////// for cf targets ////////////////////////////////////////////////// + + harness.sectionSelection.setValue(fooSpace); + assertTrue(action.isEnabled()); + assertTrue(action.isVisible()); + + ModelStateListener modelStateListener = mock(ModelStateListener.class); + fooSpace.addModelStateListener(modelStateListener); + barSpace.addModelStateListener(modelStateListener); + + doAnswer(editSetTemplate("%s - %o @ %a")) + .when(ui()).openEditTemplateDialog(any(EditTemplateDialogModel.class)); + + action.run(); + + //Changing the template should result in modelStateListener firing on all the affected models + verify(modelStateListener).stateChanged(same(fooSpace)); + verify(modelStateListener).stateChanged(same(barSpace)); + + assertEquals("foo - my-org @ https://api.some-cloud.com", fooSpace.getDisplayName()); + assertEquals("bar - your-org @ https://api.some-cloud.com", barSpace.getDisplayName()); + + //Let's also try a user interaction that involves the 'Restore Defaults' button... + + reset(ui(), modelStateListener); + + doAnswer(restoreDefaultTemplate()) + .when(ui()).openEditTemplateDialog(any(EditTemplateDialogModel.class)); + + action.run(); + + verify(modelStateListener).stateChanged(same(fooSpace)); + verify(modelStateListener).stateChanged(same(barSpace)); + + assertEquals("my-org : foo - [https://api.some-cloud.com]", fooSpace.getDisplayName()); + assertEquals("your-org : bar - [https://api.some-cloud.com]", barSpace.getDisplayName()); + } + + @Test + public void customizeTargetLabelDialog() throws Exception { + EditTemplateDialogModel dialog; + clientFactory.defSpace("my-org", "foo"); + clientFactory.defSpace("your-org", "bar"); + + String apiUrl = "https://api.some-cloud.com"; + String username = "freddy"; String password = MockCloudFoundryClientFactory.FAKE_PASSWORD; + + AbstractBootDashModel fooSpace = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "my-org", "foo", false)); + AbstractBootDashModel barSpace = harness.createCfTarget(new CFClientParams(apiUrl, username, + CFCredentials.fromLogin(LoginMethod.PASSWORD, password), false, "your-org", "bar", false)); + + ModelStateListener modelStateListener = mock(ModelStateListener.class); + fooSpace.addModelStateListener(modelStateListener); + barSpace.addModelStateListener(modelStateListener); + + // Check initial state of the dialog when no custom labels have yet been set at all: + dialog = CustomizeTargetLabelDialogModel.create(fooSpace); + + assertTrue(dialog.applyToAll.getValue()); + assertEquals("%o : %s - [%a]", dialog.template.getValue()); + + //Check performOk only changes the one label when 'apply to all' is disabled. + dialog.applyToAll.setValue(false); + dialog.template.setValue("CHANGED %s -> %o"); + dialog.performOk(); + + assertEquals("CHANGED foo -> my-org", fooSpace.getDisplayName()); + assertEquals("your-org : bar - [https://api.some-cloud.com]", barSpace.getDisplayName()); + + verify(modelStateListener).stateChanged(same(fooSpace)); + verify(modelStateListener, never()).stateChanged(same(barSpace)); + + //Opening the dialog now should have 'apply to all' disabled to avoid accidentally overwriting + // existing individually customized labels... + dialog = CustomizeTargetLabelDialogModel.create(fooSpace); + assertFalse(dialog.applyToAll.getValue()); + assertEquals("CHANGED %s -> %o", dialog.template.getValue()); + + //Also if we open the dialog on the other element!!! + dialog = CustomizeTargetLabelDialogModel.create(barSpace); + assertFalse(dialog.applyToAll.getValue()); + assertEquals("%o : %s - [%a]", dialog.template.getValue()); + + //Selecting 'apply to all' should set the template on the type and erase custom templates on the + // individual targets. + dialog.applyToAll.setValue(true); + dialog.template.setValue("DIFFERENT %s -> %o"); + dialog.performOk(); + + assertEquals("DIFFERENT %s -> %o", harness.getCfTargetType().getNameTemplate()); + for (BootDashModel target : harness.getCfRunTargetModels()) { + assertFalse(target.hasCustomNameTemplate()); + assertEquals("DIFFERENT %s -> %o", target.getNameTemplate()); + } + + assertEquals("DIFFERENT foo -> my-org", fooSpace.getDisplayName()); + assertEquals("DIFFERENT bar -> your-org", barSpace.getDisplayName()); + } + + @Test + public void testPushWithHttpHealthCheckType() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + final CloudFoundryBootDashModel model = harness.getCfTargetModel(); + + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + final String appName = appHarness.randomAppName(); + + IFile manifest = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " health-check-type: http\n" + + " health-check-http-endpoint: /health\n" + ); + harness.answerDeploymentPrompt(ui(), manifest); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + ACondition.waitFor("wait for app '"+ appName +"'to be RUNNING", 30000, () -> { + CloudAppDashElement app = model.getApplication(appName); + assertEquals(RunState.RUNNING, app.getRunState()); + assertEquals("http", app.getHealthCheck()); + assertEquals("/health", app.getHealthCheckHttpEndpoint()); + }); + } + + @Test + public void testEnvVarsSetOnFirstDeploy() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + final CloudFoundryBootDashModel model = harness.getCfTargetModel(); + + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + + final String appName = appHarness.randomAppName(); + + Map env = new HashMap<>(); + env.put("FOO", "something"); + harness.answerDeploymentPrompt(ui(), appName, appName, env); + + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + new ACondition("wait for app '"+ appName +"'to be RUNNING", 30000) { //why so long? JDT searching for main type. + public boolean test() throws Exception { + CloudAppDashElement element = model.getApplication(appName); + assertEquals(RunState.RUNNING, element.getRunState()); + return true; + } + }; + + Map actualEnv = harness.fetchEnvironment(target, appName); + + assertEquals("something", actualEnv.get("FOO")); + } + + @Test public void appToProjectBindingsPersisted() throws Exception { + final String appName = "foo"; + String projectName = "to-deploy"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + final IProject project = projects.createBootProject(projectName, withStarters("actuator", "web")); + + harness.createCfTarget(targetParams); + { + final CloudFoundryBootDashModel model = harness.getCfTargetModel(); + + deployApp(model, appName, project); + + CloudAppDashElement appByProject = getApplication(model, project); + CloudAppDashElement appByName = model.getApplication(appName); + assertNotNull(appByProject); + assertEquals(appByProject, appByName); + } + + harness.reload(); + { + final CloudFoundryBootDashModel model = harness.getCfTargetModel(); + waitForApps(model, appName); + CloudAppDashElement appByName = model.getApplication(appName); + CloudAppDashElement appByProject = getApplication(model, project); + assertNotNull(appByProject); + assertEquals(appByProject, appByName); + } + } + + @Test public void appToProjectBindingsPersistedAfterDisconnectConnect() throws Exception { + final String appName = "foo"; + String projectName = "to-deploy"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + final IProject project = projects.createBootProject(projectName, withStarters("actuator", "web")); + + final CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + deployApp(model, appName, project); + assertAppToProjectBinding(model, project, appName); + + IAction toggleConnectionAction = toggleTargetConnectionAction(); + harness.sectionSelection.setValue(model); + + toggleConnectionAction.run(); + waitForElements(model /*none*/); + toggleConnectionAction.run(); + waitForElements(model, appName); + + assertAppToProjectBinding(model, project, appName); + } + + @Test public void appToProjectBindingChangedAfterProjectRename() throws Exception { + final String appName = "foo"; + String projectName = "to-deploy"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + space.defApp(appName); + final IProject project = projects.createProject(projectName); + + final CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + waitForApps(target, appName); + CloudAppDashElement app = target.getApplication(appName); + app.setProject(project); + + assertAppToProjectBinding(target, project, appName); + + + ElementStateListener elementStateListener = mock(ElementStateListener.class); + target.addElementStateListener(elementStateListener); + + final IProject newProject = projects.rename(project, projectName+"-RENAMED"); + // resource listeners called synchronously by eclipse so we don't need ACondition + + assertAppToProjectBinding(target, newProject, appName); + + //state change event should have been fired (to update label of element in view) + verify(elementStateListener).stateChanged(same(app)); + } + + @Test public void appToProjectBindingForgottenAfterDelete() throws Exception { + final String appName = "foo"; + String projectName = "to-deploy"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + space.defApp(appName); + final IProject project = projects.createProject(projectName); + + final CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + waitForApps(target, appName); + CloudAppDashElement app = target.getApplication(appName); + app.setProject(project); + + assertAppToProjectBinding(target, project, appName); + + ElementStateListener elementStateListener = mock(ElementStateListener.class); + target.addElementStateListener(elementStateListener); + + project.delete(true, new NullProgressMonitor()); + + assertNull(app.getProject(true)); + verify(elementStateListener).stateChanged(same(app)); + } + + @Test public void runstateBecomesUnknownWhenStartOperationFails() throws Exception { + final String appName = "foo"; + String projectName = "to-deploy"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + MockCFApplication app = space.defApp(appName); + final IProject project = projects.createProject(projectName); + + final CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + waitForApps(target, appName); + CloudAppDashElement appElement = target.getApplication(appName); + appElement.setProject(project); + + //The state refressh is asynch so until state becomes 'INACTIVE' it will be unknown. + waitForState(appElement, RunState.INACTIVE, 3000); + IAction restartAction = restartOnlyApplicationAction(); + + RunStateHistory runstateHistory = new RunStateHistory(); + + appElement.getBaseRunStateExp().addListener(runstateHistory); + doThrow(IOException.class).when(app).start(any()); + + System.out.println("restarting application..."); + harness.selection.setElements(appElement); + restartAction.run(); + + waitForState(appElement, RunState.UNKNOWN, 3000); + + runstateHistory.assertHistoryContains( + RunState.INACTIVE, + RunState.STARTING + ); + runstateHistory.assertLast( + RunState.UNKNOWN + ); + } + + @Test public void refreshClearsErrorState() throws Exception { + final String appName = "foo"; + String projectName = "to-deploy"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + space.defApp(appName); + final IProject project = projects.createProject(projectName); + + final CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + waitForApps(target, appName); + CloudAppDashElement appElement = target.getApplication(appName); + appElement.setProject(project); + + waitForState(appElement, RunState.INACTIVE, 3000); + //The state refressh is asynch so until state becomes 'INACTIVE' it will be unknown. + appElement.setError(new IOException("Something bad happened")); + waitForState(appElement, RunState.UNKNOWN, 3000); + + target.refresh(ui()); + + waitForState(appElement, RunState.INACTIVE, 3000); + } + + @Test public void pushSimple() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + + final String appName = appHarness.randomAppName(); + + harness.answerDeploymentPrompt(ui(), appName, appName); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(model, appName); + + CloudAppDashElement app = model.getApplication(appName); + + waitForState(app, RunState.RUNNING, 10000); + + assertEquals((Integer)1, space.getPushCount(appName).getValue()); + + MockCFApplication deployedApp = space.getApplication(appName); + doUnchangedAppRestartTest(app, deployedApp); + } + + @Test public void pushSimpleWithManifest() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + + final String appName = appHarness.randomAppName(); + + String yaml = "applications:\n" + + "- name: "+appName+"\n" + + " memory: 512M\n" + + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 11.+}}'\n"; + IFile manifestFile = createFile(project, "manifest.yml", yaml); + + harness.answerDeploymentPrompt(ui(), manifestFile); + + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + waitForApps(model, appName); + + CloudAppDashElement app = model.getApplication(appName); + + waitForState(app, RunState.RUNNING, 10000); + + assertEquals((Integer)1, space.getPushCount(appName).getValue()); + assertEquals(manifestFile, app.getDeploymentManifestFile()); + assertEquals(512, (int) app.getMemory()); + + + MockCFApplication deployedApp = space.getApplication(appName); + CFApplication appInfo = deployedApp.getBasicInfo(); + List uris = appInfo.getUris(); + assertTrue(uris.size() == 1); + assertEquals(ImmutableList.of(appName+".cfmockapps.io"), uris); + + doUnchangedAppRestartTest(app, deployedApp); + } + + protected void doUnchangedAppRestartTest(CloudAppDashElement app, MockCFApplication deployedApp) throws Exception { + //Try to restart app. Nothing should change because we haven't changed the manifest + CFApplication appInfo = deployedApp.getBasicInfo(); + app.restart(RunState.RUNNING, ui()); + waitForJobsToComplete(); + waitForState(app, RunState.RUNNING, 10_000); + + CFApplication newAppInfo = deployedApp.getBasicInfo(); + assertSameAppState(appInfo, newAppInfo); + //If no change was detected the manifest compare dialog shouldn't have popped. + verify(ui(), never()).openManifestDiffDialog(any()); + } + + private void assertSameAppState(CFApplication appInfo, CFApplication newAppInfo) { + assertEquals(appInfoCompareString(appInfo), appInfoCompareString(newAppInfo)); + } + + private String appInfoCompareString(CFApplication app) { + StringBuilder buf = new StringBuilder(); + buf.append("name = " + app.getName()); + buf.append("instances = " + app.getInstances()); + buf.append("runningInstances = " + app.getRunningInstances()); + buf.append("memory = " + app.getMemory()); + buf.append("memory = " + app.getMemory()); + buf.append("guid = " + app.getGuid()); + buf.append("services = " + app.getServices()); + buf.append("buildpack = " + app.getBuildpackUrl()); + buf.append("uris = " + app.getUris()); + buf.append("state = " + app.getState()); + buf.append("diskQuota = " + app.getDiskQuota()); + buf.append("timeout = " + app.getTimeout()); + buf.append("command = " + app.getCommand()); + buf.append("stack = " + app.getStack()); + return buf.toString(); + } + + + @Test public void pushSimpleWithDefaultManualManifest() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + + final String appName = project.getName(); + + harness.answerDeploymentPrompt(ui(), new DeploymentAnswerer()); + + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + waitForApps(model, appName); + + CloudAppDashElement app = model.getApplication(appName); + + waitForState(app, RunState.RUNNING, 10000); + + assertEquals((Integer)1, space.getPushCount(appName).getValue()); + assertNull(app.getDeploymentManifestFile()); + assertEquals(1024, (int) app.getMemory()); + assertEquals(appName, app.getName()); + + MockCFApplication deployedApp = space.getApplication(appName); + doUnchangedAppRestartTest(app, deployedApp); + } + + @Test public void warDeploy() throws Exception { + MavenSpringBootProject.DUMP_MAVEN_OUTPUT = true; + try { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + IProject project = projects.createBootProject("to-deploy-war", + withStarters("actuator", "web"), + withPackaging("war") + ); + assertEquals("war", springBootCore.project(project).getPackaging()); + + String appName = project.getName(); + harness.answerDeploymentPrompt(ui(), new DeploymentAnswerer()); + + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(model, appName); + + CloudAppDashElement app = model.getApplication(appName); + waitForState(app, RunState.RUNNING, 10000); + + assertEquals((Integer)1, space.getPushCount(appName).getValue()); + assertNull(app.getDeploymentManifestFile()); + assertEquals(1024, (int) app.getMemory()); + assertEquals(appName, app.getName()); + + File projectLocation = project.getLocation().toFile(); + File warFile = new File(projectLocation, "target/"+project.getName()+"-0.0.1-SNAPSHOT.war"); + assertTrue("war file not found: "+warFile, warFile.exists()); + assertDeployedBytes(warFile, space.getApplication(appName)); + } finally { + MavenSpringBootProject.DUMP_MAVEN_OUTPUT = false; + } + } + + @Test public void stopCancelsDeploy() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + + final String appName = appHarness.randomAppName(); + + clientFactory.setAppStartDelay(TimeUnit.MINUTES, 2); + harness.answerDeploymentPrompt(ui(), appName, appName); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(model, appName); + + CloudAppDashElement app = model.getApplication(appName); + + waitForState(app, RunState.STARTING, 3000); + + ACondition.waitFor("stop hammering", 20000, () -> { + app.stopAsync(); + assertEquals(RunState.INACTIVE, app.getRunState()); + }); + + //TODO: can we check that deployment related jobs are really canceled/finished somehow? + // can we check that they didn't pop errors? + } + + + + @Test public void stopCancelsStart() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + final String appName = "foo"; + space.defApp(appName); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForApps(model, appName); + CloudAppDashElement app = model.getApplication(appName); + app.setProject(project); + + waitForApps(model, appName); + + clientFactory.setAppStartDelay(TimeUnit.MINUTES, 2); + + app.getBaseRunStateExp().addListener(new ValueListener() { + @Override + public void gotValue(LiveExpression exp, RunState value) { + System.out.println("Runstate -> "+value); + } + }); + System.out.println("Restaring app..."); + app.restart(RunState.RUNNING, ui()); + waitForState(app, RunState.STARTING, 30000); + + System.out.println("Stopping app..."); + app.stopAsync(); + + waitForState(app, RunState.INACTIVE, 20000); + System.out.println("Stopped!"); + } + + @Test public void stopCancelsRestartOnly() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + IProject project = projects.createProject("to-deploy"); + final String appName = "foo"; + space.defApp(appName); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForApps(model, appName); + CloudAppDashElement app = model.getApplication(appName); + app.setProject(project); + + waitForApps(model, appName); + + clientFactory.setAppStartDelay(TimeUnit.MINUTES, 2); + app.restartOnlyAsynch(ui(), app.createCancelationToken()); + waitForState(app, RunState.STARTING, 3000); + + app.stopAsync(); + waitForState(app, RunState.INACTIVE, 20000); + } + + @Test public void acceptDeployOfExistingApp() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + IProject project = projects.createBootProject("to-deploy", withStarters("actuator", "web")); + final String appName = "foo"; + MockCFApplication deployedApp = space.defApp(appName); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForApps(model, appName); + CloudAppDashElement app = model.getApplication(appName); + app.setProject(null); + assertNull(app.getProject()); + waitForState(app, RunState.INACTIVE, 3000); + + harness.answerDeploymentPrompt(ui(), appName, appName); + Mockito.doReturn(ManifestDiffDialogModel.Result.USE_MANIFEST).when(ui()).confirmReplaceApp(any(), any(), any(), any()); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + System.out.println(app.getRunState()); + waitForJobsToComplete(); + System.out.println(app.getRunState()); + assertEquals(project, app.getProject()); + assertEquals(1, deployedApp.getPushCount()); + + verify(ui()).confirmReplaceApp(any(), any(), any(), any()); + } + + + @Test public void cancelDeployOfExistingApp() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + final String appName = "foo"; + MockCFApplication deployedApp = space.defApp(appName); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForApps(model, appName); + CloudAppDashElement app = model.getApplication(appName); + app.setProject(null); + assertNull(app.getProject()); + + harness.answerDeploymentPrompt(ui(), appName, appName); + doReturn(ManifestDiffDialogModel.Result.CANCELED).when(ui()).confirmReplaceApp(any(), any(), any(), any()); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + waitForJobsToComplete(); + assertNull(app.getProject()); // since op was canceled it should not have set the project on the app. + assertEquals(0, deployedApp.getPushCount()); // since op was canceled it should not have deployed the app. + + verify(ui()).confirmReplaceApp(any(), any(), any(), any()); + } + + @Test public void manifestDiffDialogNotShownWhenNothingChanged() throws Exception { + final String appName = "foo"; + + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + IFile manifest = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ jre: { version: 11.+}}'\n" + ); + harness.answerDeploymentPrompt(ui(), manifest); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(model, appName); + CloudAppDashElement app = model.getApplication(appName); + waitForState(app, RunState.RUNNING, 10_000); + assertEquals(1, app.getActualInstances()); + +// deployedApp.scaleInstances(2); // change it 'externally' + assertEquals(1, app.getActualInstances()); //The model doesn't know yet that it has changed! + +// harness.answerDeploymentPrompt(ui(), appName, appName); + app.restart(RunState.RUNNING, ui()); + waitForJobsToComplete(); + + //If no change was detected the manifest compare dialog shouldn't have popped. + verify(ui(), never()).openManifestDiffDialog(any()); + } + + @Test public void manifestDiffDialogShownWhenInstancesChangedExternally() throws Exception { + final String appName = "foo"; + + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + ); + + harness.answerManifestDiffDialog(ui(), (ManifestDiffDialogModel dialog) -> { + //??? code to check what's in the dialog??? + return ManifestDiffDialogModel.Result.CANCELED; + }); + + MockCFApplication deployedApp = space.defApp(appName); + deployedApp.start(CancelationTokens.NULL); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForApps(model, appName); + waitForJobsToComplete(); + + CloudAppDashElement app = model.getApplication(appName); + app.setDeploymentManifestFile(project.getFile("manifest.yml")); + app.setProject(project); + assertEquals(1, app.getActualInstances()); + + deployedApp.scaleInstances(2); // change it 'externally' + assertEquals(1, app.getActualInstances()); //The model doesn't know yet that it has changed! + + app.restart(RunState.RUNNING, ui()); + + waitForJobsToComplete(); + + //If the change was detected the deployment props dialog should have popped exactly once. + verify(ui()).openManifestDiffDialog(any()); + } + + @Test public void manifestDiffDialogChooseUseManfifest() throws Exception { + //Setup initial state for our test + final String appName = "foo"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + IFile manifest = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " memory: 1111M\n" + ); + + harness.answerDeploymentPrompt(ui(), manifest); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + waitForApps(model, appName); + CloudAppDashElement app = model.getApplication(appName); + waitForState(app, RunState.RUNNING, APP_DEPLOY_TIMEOUT); + + { + MockCFApplication appInCloud = space.getApplication(appName); + assertEquals(1111, appInCloud.getMemory()); + Mockito.reset(ui()); + + //// real test begins here + + appInCloud.setMemory(2222); + } + + harness.answerManifestDiffDialog(ui(), (ManifestDiffDialogModel dialog) -> { + //??? code to check what's in the dialog??? + return ManifestDiffDialogModel.Result.USE_MANIFEST; + }); + + app.restart(RunState.RUNNING, ui()); + + waitForJobsToComplete(); + { + MockCFApplication appInCloud = space.getApplication(appName); + assertEquals(2, appInCloud.getPushCount()); + assertEquals(RunState.RUNNING, app.getRunState()); + assertEquals(1111, appInCloud.getMemory()); + assertEquals(1111, (int)app.getMemory()); + } + } + + @Test public void manifestDiffDialogChooseForgetManfifest() throws Exception { + //Setup initial state for our test + final String appName = "foo"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + IProject project = projects.createBootProject("to-deploy", withStarters("web", "actuator")); + IFile manifest = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " memory: 1111M\n" + ); + + harness.answerDeploymentPrompt(ui(), manifest); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + waitForApps(model, appName); + CloudAppDashElement app = model.getApplication(appName); + waitForState(app, RunState.RUNNING, APP_DEPLOY_TIMEOUT); + + MockCFApplication appInCloud = space.getApplication(appName); + assertEquals(1111, appInCloud.getMemory()); + Mockito.reset(ui()); + + //// real test begins here + + appInCloud.setMemory(2222); + + harness.answerManifestDiffDialog(ui(), (ManifestDiffDialogModel dialog) -> { + //??? code to check what's in the dialog??? + return ManifestDiffDialogModel.Result.FORGET_MANIFEST; + }); + + app.restart(RunState.RUNNING, ui()); + + waitForJobsToComplete(); + + assertEquals(2, appInCloud.getPushCount()); + assertEquals(RunState.RUNNING, app.getRunState()); + assertEquals(2222, appInCloud.getMemory()); + assertEquals(2222, (int)app.getMemory()); + } + + @Test public void testDeployManifestWithAbsolutePathAttribute() throws Exception { + final String appName = "foo"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + IProject project = projects.createProject("to-deploy"); + + File zipFile = getTestZip("testapp"); + + IFile manifestFile = createFile(project, "manifest.yml", + "applications:\n" + + "- name: foo\n" + + " path: "+zipFile.getAbsolutePath() + "\n" + + " buildpack: staticfile_buildpack" + ); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + harness.answerDeploymentPrompt(ui(), manifestFile); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + waitForApps(model, "foo"); + + CloudAppDashElement app = model.getApplication("foo"); + waitForState(app, RunState.RUNNING, APP_DELETE_TIMEOUT); + + assertEquals(project, app.getProject()); + + assertEquals("some content here\n", space.getApplication(appName).getFileContents("test.txt")); + + verify(ui()).promptApplicationDeploymentProperties(any()); + verifyNoMoreInteractions(ui()); + } + + + @Test public void testDeployManifestWithRelativePathAttribute() throws Exception { + final String appName = "foo"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + IProject project = projects.createProject("to-deploy"); + + File zipFile = getTestZip("testapp"); + project.getFolder("zips").create(true, true, new NullProgressMonitor()); + project.getFolder("manifests").create(true, true, new NullProgressMonitor()); + createFile(project, "zips/testapp.zip", zipFile); + + IFile manifestFile = createFile(project, "manifests/manifest.yml", + "applications:\n" + + "- name: foo\n" + + " path: ../zips/testapp.zip\n" + + " buildpack: staticfile_buildpack" + ); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + harness.answerDeploymentPrompt(ui(), manifestFile); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + waitForApps(model, "foo"); + + CloudAppDashElement app = model.getApplication("foo"); + waitForState(app, RunState.RUNNING, APP_DEPLOY_TIMEOUT); + + assertEquals(project, app.getProject()); + + assertEquals("some content here\n", space.getApplication(appName).getFileContents("test.txt")); + + verify(ui()).promptApplicationDeploymentProperties(any()); + verifyNoMoreInteractions(ui()); + } + + @Test public void testDeployManifestWithoutPathAttribute() throws Exception { + String appName = "foo"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + IProject project = projects.createBootWebProject("empty-web-app"); + IFile manifestFile = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + ); + File referenceJar = BootJarPackagingTest.packageAsJar(project, ui()); + + harness.answerDeploymentPrompt(ui(), manifestFile); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(model, appName); + + CloudAppDashElement app = model.getApplication(appName); + waitForState(app, RunState.RUNNING, APP_DEPLOY_TIMEOUT); + + System.out.println("platform location = '"+Platform.getLocation()+"'"); + assertDeployedBytes(referenceJar, space.getApplication(appName)); + } + + @Test public void testSelectManifestActionEnablement() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + IProject project1 = projects.createProject("pr1"); + IProject project2 = projects.createProject("pr2"); + + final String appName1 = "app1"; + final String appName2 = "app2"; + + MockCFApplication cfApp1 = space.defApp(appName1); + MockCFApplication cfApp2 = space.defApp(appName2); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForApps(model, appName1, appName2); + + CloudAppDashElement app1 = model.getApplication(appName1); + CloudAppDashElement app2 = model.getApplication(appName2); + + app1.setProject(project1); + app2.setProject(project2); + + IAction action = selectManifestAction(); + + assertTrue(harness.selection.getElements().isEmpty()); + assertFalse(action.isEnabled()); + + harness.selection.setElements(ImmutableSet.of(app1)); + assertNotNull(app1.getProject()); + assertTrue(action.isEnabled()); + + harness.selection.setElements(ImmutableSet.of(app1, app2)); + assertFalse(action.isEnabled()); + + app1.setProject(null); + harness.selection.setElements(ImmutableSet.of(app1)); + assertFalse(action.isEnabled()); + + harness.selection.setElements(ImmutableSet.of(app2)); + assertTrue(action.isEnabled()); + action.run(); + + } + + @Test public void testSelectManifestAction() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + IProject project = projects.createProject("pr"); + + final String appName = "app"; + + IFile manifestFile = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + ); + + space.defApp(appName); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForApps(model, appName); + + CloudAppDashElement app = model.getApplication(appName); + app.setProject(project); + + harness.selection.setElements(ImmutableSet.of(app)); + + harness.answerDeploymentPrompt(ui(), manifestFile); + + assertNull(app.getDeploymentManifestFile()); + selectManifestAction().run(); + waitForJobsToComplete(); + assertEquals(manifestFile, app.getDeploymentManifestFile()); + + verify(ui()).promptApplicationDeploymentProperties(any()); + verifyNoMoreInteractions(ui()); + } + + @Test public void disconnectTarget() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + String appName = "someApp"; + space.defApp(appName); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForApps(model, appName); + assertEquals(1, clientFactory.instanceCount()); + + harness.sectionSelection.setValue(model); + IAction disconnectAction = toggleTargetConnectionAction(); + assertTrue(disconnectAction.isEnabled()); + disconnectAction.run(); + waitForApps(model); + assertFalse(model.isConnected()); + waitForModelReady(model); + assertEquals(0, clientFactory.instanceCount()); + } + + @Test public void updateTargetPasswordInvalid() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + String appName = "someApp"; + space.defApp(appName); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + waitForApps(model, appName); + + harness.sectionSelection.setValue(model); + IAction updatePassword = updatePasswordAction(); + assertTrue(updatePassword.isEnabled()); + + harness.answerPasswordPrompt(ui(), (d) -> { + d.getPasswordVar().setValue(targetParams.getCredentials().getSecret()); + d.validateCredentials().block(); + d.performOk(); + }); + + updatePassword.run(); + + waitForJobsToComplete(); + + assertNotNull(model.getApplication(appName)); + assertTrue(model.isConnected()); + waitForModelReady(model); + + // Clear out any mocks on the ui object set above + reset(ui()); + + CompletableFuture passwordValidation = new CompletableFuture<>(); + + harness.answerPasswordPrompt(ui(), (d) -> { + d.getPasswordVar().setValue("wrong password"); + ReactorUtils.completeWith(passwordValidation, d.validateCredentials()); + //d.performOk(); //shouldn't perform ok because we are expecting passwordValidation to fail + }); + + updatePassword.run(); + + ValidationResult validationResult = passwordValidation.get(); + assertEquals(IStatus.ERROR, validationResult.status); + assertContains("Invalid credentials", validationResult.msg); + + } + + @Test public void pushWithHealthCheckProcess() throws Exception { + doPushWithHealthCheckType("process", "process"); + } + + @Test public void pushWithHealthCheckDefault() throws Exception { + doPushWithHealthCheckType(null, "port"); + } + + @Test public void pushWithHealthCheckPort() throws Exception { + doPushWithHealthCheckType("port", "port"); + } + + private void doPushWithHealthCheckType(String specified, String expected) throws Exception { + String appName = "someApp"; + final IProject project = projects.createBootProject(appName, withStarters("actuator", "web")); + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + + final String yaml = "applications:\n" + + "- name: "+appName+"\n" + + (specified==null?"":" health-check-type: "+specified+"\n"); + + harness.answerDeploymentPrompt(ui(), new DeploymentAnswerer(yaml)); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(model, appName); + waitForState(model.getApplication(appName), RunState.RUNNING, 4000); + waitForJobsToComplete(); + + assertEquals(expected,space.getApplication(appName).getHealthCheckType()); + ACondition.waitFor("hcType in model", 3000, () -> { + assertEquals(expected, model.getApplication(appName).getHealthCheck()); + }); + + MockCFApplication deployedApp = space.getApplication(appName); + doUnchangedAppRestartTest(model.getApplication(appName), deployedApp); + } + + @Test public void updateTargetSsoAndStoreNothing() throws Exception { + String appName = "someApp"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + space.defApp(appName); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams, StoreCredentialsMode.STORE_TOKEN); + waitForApps(model, appName); + + harness.sectionSelection.setValue(model); + IAction updatePassword = updatePasswordAction(); + assertTrue(updatePassword.isEnabled()); + + // Clear out any mocks on the ui object + reset(ui()); + + String newToken=clientFactory.getSsoToken(); + harness.answerPasswordPrompt(ui(), (d) -> { + d.getMethodVar().setValue(LoginMethod.TEMPORARY_CODE); + d.getPasswordVar().setValue(newToken); + d.getStoreVar().setValue(StoreCredentialsMode.STORE_NOTHING); + d.validateCredentials().block(); + d.performOk(); + }); + + updatePassword.run(); + + waitForJobsToComplete(); + + assertTrue(model.isConnected()); + assertNotNull(model.getApplication(appName)); + waitForModelReady(model); + + { + assertNull(harness.getCredentialsStore().getCredentials(harness.secureStoreKey(model))); + assertNull(harness.getPrivateStore().get(harness.privateStoreKey(model))); + } + + toggleTargetConnectionAction().run(); + + waitForJobsToComplete(); + assertFalse(model.isConnected()); + + // Clear out any mocks on the ui object to get the right count below + reset(ui()); + + toggleTargetConnectionAction().run(); + waitForJobsToComplete(); + + assertTrue(model.isConnected()); + waitForModelReady(model); + waitForApps(model, appName); + + verifyNoMoreInteractions(ui()); + } + + @Test public void updateTargetSsoAndStorePassword() throws Exception { + // In real PWS this scenario would use a 'single use' password that can be obtained + // from https://login.run.pivotal.io/passcode + String appName = "someApp"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + space.defApp(appName); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams, StoreCredentialsMode.STORE_TOKEN); + waitForApps(model, appName); + + harness.sectionSelection.setValue(model); + IAction updatePassword = updatePasswordAction(); + assertTrue(updatePassword.isEnabled()); + + // Clear out any mocks on the ui object + reset(ui()); + + LiveVariable capturedStoreValidator = new LiveVariable<>(null); + + String newToken=clientFactory.getSsoToken(); + harness.answerPasswordPrompt(ui(), (d) -> { + d.getMethodVar().setValue(LoginMethod.TEMPORARY_CODE); + d.getPasswordVar().setValue(newToken); + d.getStoreVar().setValue(StoreCredentialsMode.STORE_PASSWORD); + capturedStoreValidator.setValue(d.getStoreValidator().getValue()); + d.validateCredentials().block(); + d.performOk(); + }); + + updatePassword.run(); + + waitForJobsToComplete(); + + assertContains("'Store Password' is useless for a 'Temporary Code'", capturedStoreValidator.getValue().msg); + + assertTrue(model.isConnected()); + assertNotNull(model.getApplication(appName)); + waitForModelReady(model); + //store password is ignored and treated as 'STORE_NOTHING' + assertEquals(StoreCredentialsMode.STORE_NOTHING, model.getRunTarget().getTargetProperties().getStoreCredentials()); + { + assertNull(harness.getCredentialsStore().getCredentials(harness.secureStoreKey(model))); + assertNull(harness.getPrivateStore().get(harness.privateStoreKey(model))); + } + + toggleTargetConnectionAction().run(); + + waitForJobsToComplete(); + assertFalse(model.isConnected()); + + // Clear out any mocks on the ui object to get the right count below + reset(ui()); + + toggleTargetConnectionAction().run(); + waitForJobsToComplete(); + + assertTrue(model.isConnected()); + waitForModelReady(model); + waitForApps(model, appName); + + verifyNoMoreInteractions(ui()); + } + + @Test public void updateTargetSsoAndStoreToken() throws Exception { + String appName = "someApp"; + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + space.defApp(appName); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams, StoreCredentialsMode.STORE_PASSWORD); + waitForApps(model, appName); + + harness.sectionSelection.setValue(model); + IAction updatePassword = updatePasswordAction(); + assertTrue(updatePassword.isEnabled()); + + // Clear out any mocks on the ui object + reset(ui()); + + String newToken=clientFactory.getSsoToken(); + harness.answerPasswordPrompt(ui(), (d) -> { + d.getMethodVar().setValue(LoginMethod.TEMPORARY_CODE); + d.getPasswordVar().setValue(newToken); + d.getStoreVar().setValue(StoreCredentialsMode.STORE_TOKEN); + d.validateCredentials().block(); + d.performOk(); + }); + + updatePassword.run(); + + waitForJobsToComplete(); + + assertTrue(model.isConnected()); + assertNotNull(model.getApplication(appName)); + waitForModelReady(model); + + assertEquals(StoreCredentialsMode.STORE_TOKEN, model.getRunTarget().getTargetProperties().getStoreCredentials()); + { + assertNull(harness.getCredentialsStore().getCredentials(harness.secureStoreKey(model))); + assertEquals(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN, harness.getPrivateStore().get(harness.privateStoreKey(model))); + } + + toggleTargetConnectionAction().run(); + + waitForJobsToComplete(); + assertFalse(model.isConnected()); + + // Clear out any mocks on the ui object to get the right count below + reset(ui()); + + toggleTargetConnectionAction().run(); + waitForJobsToComplete(); + + assertTrue(model.isConnected()); + waitForModelReady(model); + assertNotNull(model.getApplication(appName)); + + verifyZeroInteractions(ui()); //should have used stored token so no pw dialog! + } + + @Test public void updateTargetPasswordAndStoreNothing() throws Exception { + /* + +store.put(new Key(element, key), value); + + key = from constant org.springframework.ide.eclipse.boot.dash.model.RunTargetPropertiesManager.RUN_TARGET_KEY + element = CloudFoundrryRuntargetTupe instance + value = array (vai ArrayEncoder of serialed CloudRuntargetProperties for example: + + { + "storeCredentials":"STORE_PASSWORD", + "runTargetID":"kdevolder@gopivotal.com : https://api.run.pivotal.io : application-platform-testing : sts-20200309-kdvolder-xbctakulkwcy", + "selfsigned":"false","organization":"application-platform-testing", + "skipSslValidation":"false", + "organization_guid":"7508f3b4-ec6f-459a-b1e1-e209f72f6fe3", + "space_guid":"97124746-5c09-4e23-adda-7ed43410c0d4", + "url":"https://api.run.pivotal.io", + "space":"sts-20200309-kdvolder-xbctakulkwcy", + "username":"kdevolder@gopivotal.com" + }; + + */ + + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + String appName = "someApp"; + space.defApp(appName); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForApps(model, appName); + + assertStoredStoreMode(StoreCredentialsMode.STORE_PASSWORD, model); + + harness.sectionSelection.setValue(model); + IAction updatePassword = updatePasswordAction(); + assertTrue(updatePassword.isEnabled()); + + // Clear out any mocks on the ui object + reset(ui()); + + harness.answerPasswordPrompt(ui(), (d) -> { + d.getPasswordVar().setValue(targetParams.getCredentials().getSecret()); + d.getStoreVar().setValue(StoreCredentialsMode.STORE_NOTHING); + d.validateCredentials().block(); + d.performOk(); + }); + + updatePassword.run(); + + waitForJobsToComplete(); + + + assertTrue(model.isConnected()); + assertNotNull(model.getApplication(appName)); + waitForModelReady(model); + + { + assertStoredStoreMode(StoreCredentialsMode.STORE_NOTHING, model); + assertNull(harness.getCredentialsStore().getCredentials(harness.secureStoreKey(model))); + assertNull(harness.getPrivateStore().get(harness.privateStoreKey(model))); + } + + toggleTargetConnectionAction().run(); + + waitForJobsToComplete(); + assertFalse(model.isConnected()); + + // Clear out any mocks on the ui object to get the right count below + reset(ui()); + + toggleTargetConnectionAction().run(); + waitForJobsToComplete(); + + assertTrue(model.isConnected()); + waitForModelReady(model); + assertNotNull(model.getApplication(appName)); + + verifyNoMoreInteractions(ui()); + } + + private void assertStoredStoreMode(StoreCredentialsMode expectedStoreMode, CloudFoundryBootDashModel model) { + CloudFoundryRunTargetType cfType = model.getRunTarget().getType(); + String[] runTargetsJson = ArrayEncoder.decode(harness.context.getRunTargetProperties().get(cfType, RunTargetPropertiesManager.RUN_TARGET_KEY)); + assertEquals(1, runTargetsJson.length); + CloudFoundryTargetProperties storedParams = cfType.parseParams(runTargetsJson[0]); + assertEquals(expectedStoreMode, storedParams.getStoreCredentials()); + } + + @Test public void updateTargetPasswordAndStorePassword() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + String appName = "someApp"; + space.defApp(appName); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + waitForApps(model, appName); + + harness.sectionSelection.setValue(model); + IAction updatePassword = updatePasswordAction(); + assertTrue(updatePassword.isEnabled()); + + // Clear out any mocks on the ui object + reset(ui()); + + harness.answerPasswordPrompt(ui(), (d) -> { + d.getPasswordVar().setValue(targetParams.getCredentials().getSecret()); + d.getStoreVar().setValue(StoreCredentialsMode.STORE_PASSWORD); + d.validateCredentials().block(); + d.performOk(); + }); + + updatePassword.run(); + + waitForJobsToComplete(); + + toggleTargetConnectionAction().run(); + + waitForJobsToComplete(); + assertFalse(model.isConnected()); + + // Clear out any mocks on the ui object to get the right count below + reset(ui()); + + toggleTargetConnectionAction().run(); + waitForJobsToComplete(); + + assertTrue(model.isConnected()); + waitForModelReady(model); + assertNotNull(model.getApplication(appName)); + + verifyNoMoreInteractions(ui()); + } + + @Test public void updateTargetPasswordAndStoreToken() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + String appName = "someApp"; + space.defApp(appName); + CloudFoundryBootDashModel target = harness.createCfTarget(targetParams); + waitForApps(target, appName); + + harness.sectionSelection.setValue(target); + IAction updatePassword = updatePasswordAction(); + assertTrue(updatePassword.isEnabled()); + + // Clear out any mocks on the ui object + reset(ui()); + + harness.answerPasswordPrompt(ui(), (d) -> { + d.getPasswordVar().setValue(targetParams.getCredentials().getSecret()); + d.getStoreVar().setValue(StoreCredentialsMode.STORE_TOKEN); + d.validateCredentials().block(); + d.performOk(); + }); + + updatePassword.run(); + waitForJobsToComplete(); + assertTrue(target.isConnected()); + assertEquals(1, target.activeRefreshTokenListeners.get()); + + toggleTargetConnectionAction().run(); //disconnect + + waitForJobsToComplete(); + assertFalse(target.isConnected()); + assertEquals(0, target.activeRefreshTokenListeners.get()); + + // Clear out any mocks on the ui object to get the right count below + reset(ui()); + + toggleTargetConnectionAction().run(); + waitForJobsToComplete(); + + assertTrue(target.isConnected()); + CFCredentials creds = target.getRunTarget().getTargetProperties().getCredentials(); + assertEquals(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN, creds.getSecret()); + assertEquals(CFCredentialType.REFRESH_TOKEN, creds.getType()); + assertEquals(MockCloudFoundryClientFactory.FAKE_REFRESH_TOKEN, harness.getPrivateStore().get(harness.privateStoreKey(target))); + assertNull(harness.getCredentialsStore().getCredentials(harness.secureStoreKey(target))); + assertEquals(RefreshState.READY, target.getRefreshState()); + assertNotNull(target.getApplication(appName)); + + verifyNoMoreInteractions(ui()); + + clientFactory.changeRefrestToken("another-1"); + clientFactory.changeRefrestToken("another-2"); + ACondition.waitFor("changed stored token", 300, () -> { + assertEquals("another-2", getStoredToken(target)); + }); + } + + @Test public void apiVersionCheck() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + + clientFactory.setApiVersion("1.1.0"); + clientFactory.setSupportedApiVersion("1.1.1"); + + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + ACondition.waitFor("connected to target", 10_000, () -> model.isConnected()); + + waitForJobsToComplete(); + + assertTrue(model.getRefreshState().isWarning()); + String msg = model.getRefreshState().getMessage(); + assertContains("may not work as expected", msg); + assertContains("1.1.1", msg); + assertContains("1.1.0", msg); + } + + @Test public void clientDisposedWhenTargetRemoved() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + space.defApp("bonkers"); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + ACondition.waitFor("connected to target", 10_000, () -> { + assertTrue(model.isConnected()); + assertEquals(RefreshState.READY, model.getRefreshState()); + }); + waitForApps(model, "bonkers"); + + assertEquals(1, clientFactory.instanceCount()); + + harness.sectionSelection.setValue(model); + when(ui().confirmOperation(contains("Deleting"), any())) + .thenReturn(true); + actions.getRemoveRunTargetAction().run(); + + ACondition.waitFor("target removed", 10_000, () -> { + harness.model.getSectionModels().getValue().isEmpty(); + }); + assertEquals(0, clientFactory.instanceCount()); + } + + @Test public void oldClientDisposedWhenClientCredentialsUpdated() throws Exception { + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + MockCFSpace space = clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + space.defApp("bonkers"); + CloudFoundryBootDashModel model = harness.createCfTarget(targetParams); + ACondition.waitFor("connected to target", 10_000, () -> { + assertTrue(model.isConnected()); + assertEquals(RefreshState.READY, model.getRefreshState()); + }); + waitForApps(model, "bonkers"); + assertEquals(1, clientFactory.instanceCount()); + + clientFactory.setPassword("something-else"); + + harness.sectionSelection.setValue(model); + harness.answerPasswordPrompt(ui(), (PasswordDialogModel passwordDialog) -> { + passwordDialog.getPasswordVar().setValue("something-else"); + assertEquals(ValidationResult.OK, passwordDialog.validateCredentials().block()); + passwordDialog.performOk(); + }); + harness.sectionSelection.setValue(model); + UpdatePasswordAction updatePW = updatePasswordAction(); + updatePW.run(); + updatePW.waitFor(); + + ACondition.waitFor("connected to target", 10_000, () -> { + assertTrue(model.isConnected()); + assertEquals(RefreshState.READY, model.getRefreshState()); + }); + assertEquals(1, clientFactory.instanceCount()); + } + + @Test public void openAppWithPathInBrowser() throws Exception { + final String appName = "to-deploy"; + final String appPath = "/the-path"; + String projectName = "to-deploy"; + final String host = "foo-host"; + + CFClientParams targetParams = CfTestTargetParams.fromEnv(); + clientFactory.defSpace(targetParams.getOrgName(), targetParams.getSpaceName()); + final IProject project = projects.createBootProject(projectName, withStarters("actuator", "web")); + harness.createCfTarget(targetParams); + final CloudFoundryBootDashModel target = harness.getCfTargetModel(); + IFile manifest = createFile(project, "manifest.yml", + "applications:\n" + + "- name: "+appName+"\n" + + " routes:\n" + + " - route: "+host+".cfmockapps.io"+appPath + ); + harness.answerDeploymentPrompt(ui(), manifest); + target.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + waitForApps(target, appName); + + CloudAppDashElement app = target.getApplication(appName); + ACondition.waitFor("uri update", 5_000, () -> { + assertEquals("foo-host.cfmockapps.io", app.getLiveHost()); + assertEquals("https://foo-host.cfmockapps.io/the-path", app.getUrl()); + }); + + app.setDefaultRequestMappingPath("/hello"); + ACondition.waitFor("uri update", 5_000, () -> { + assertEquals("foo-host.cfmockapps.io", app.getLiveHost()); + assertEquals("https://foo-host.cfmockapps.io/the-path/hello", app.getUrl()); + }); + + app.setDefaultRequestMappingPath("/"); + ACondition.waitFor("uri update", 5_000, () -> { + assertEquals("foo-host.cfmockapps.io", app.getLiveHost()); + assertEquals("https://foo-host.cfmockapps.io/the-path/", app.getUrl()); + }); + + app.setDefaultRequestMappingPath("hello"); + ACondition.waitFor("uri update", 5_000, () -> { + assertEquals("foo-host.cfmockapps.io", app.getLiveHost()); + assertEquals("https://foo-host.cfmockapps.io/the-path/hello", app.getUrl()); + }); + + app.setDefaultRequestMappingPath(""); + ACondition.waitFor("uri update", 5_000, () -> { + assertEquals("foo-host.cfmockapps.io", app.getLiveHost()); + assertEquals("https://foo-host.cfmockapps.io/the-path", app.getUrl()); + }); + + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + + //Stuff below is 'cruft' intended to make the tests above more readable. Maybe this code could be + // moved to some kind of 'harness' (if there is a case where it can be reused). + + private void assertDeployedBytes(File referenceJar, MockCFApplication app) throws IOException { + try (InputStream actualBits = app.getBits()) { + try (InputStream expectedBits = new BufferedInputStream(new FileInputStream(referenceJar))) { + ZipDiff zipDiff = new ZipDiff(expectedBits); + try { + zipDiff.assertEqual(actualBits); + } catch (Throwable e) { + System.out.println("Failed: "+ExceptionUtil.getMessage(e)); + saveArtefacts(referenceJar, app); + throw e; + } + } + } + } + + private void saveArtefacts(File referenceJar, MockCFApplication app) throws IOException { + System.out.println("Trying to save jars..."); + File targetDir = getSaveDir(); + System.out.println("targetDir = "+targetDir); + if (targetDir!=null) { + int id = uniqueId++; + File referenceJarCopy = new File(targetDir, "deployed-reference-"+id+".jar"); + File faultyJarCopy = new File(targetDir, "deployed-faulty-"+id+".jar"); + FileUtils.copyFile(referenceJar, referenceJarCopy); + System.out.println("Reference jar saved: "+referenceJarCopy); + IOUtil.pipe(app.getBits(), faultyJarCopy); + System.out.println("Faulty jar saved: "+faultyJarCopy); + } + } + + private static int uniqueId = 0; + + private File getSaveDir() { + IPath targetDirPath = Platform.getLocation(); + while (targetDirPath.segmentCount()>0 && !targetDirPath.lastSegment().equals("target")) { + targetDirPath = targetDirPath.removeLastSegments(1); + } + if (targetDirPath.segmentCount()>0) { + return targetDirPath.toFile(); + } + return new File(System.getProperty("user.home")); + } + + private File getTestZip(String fileName) { + File sourceWorkspace = new File( + StsTestUtil.getSourceWorkspacePath("org.springframework.ide.eclipse.boot.dash.test")); + File file = new File(sourceWorkspace, fileName + ".zip"); + Assert.isTrue(file.exists(), ""+ file); + return file; + } + + private void assertAppToProjectBinding(CloudFoundryBootDashModel target, IProject project, String appName) throws Exception { + CloudAppDashElement appByProject = getApplication(target, project); + CloudAppDashElement appByName = target.getApplication(appName); + assertNotNull(appByProject); + assertEquals(appByProject, appByName); + } + + private CloudAppDashElement getApplication(CloudFoundryBootDashModel model, IProject project) { + for (CloudAppDashElement app : model.getApplicationValues()) { + IProject p = app.getProject(); + if (project.equals(p)) { + return app; + } + } + return null; + } + + protected CloudAppDashElement deployApp(final CloudFoundryBootDashModel model, final String appName, IProject project) + throws Exception { + harness.answerDeploymentPrompt(ui(), appName, appName); + model.performDeployment(ImmutableSet.of(project), RunState.RUNNING); + + waitForApps(model, appName); + + new ACondition("wait for app '"+ appName +"'to be RUNNING", 30000) { //why so long? JDT searching for main type. + public boolean test() throws Exception { + CloudAppDashElement element = model.getApplication(appName); + assertEquals(RunState.RUNNING, element.getRunState()); + return true; + } + }; + return model.getApplication(appName); + } + + protected void waitForModelReady(CloudFoundryBootDashModel model) throws Exception { + ACondition.waitFor("Ready refresh state", 2_000, () -> { + assertEquals(RefreshState.READY, model.getRefreshState()); + }); + } + + protected void waitForApps(final CloudFoundryBootDashModel target, final String... names) throws Exception { + new ACondition("wait for apps to appear", 120_000) { + @Override + public boolean test() throws Exception { + ImmutableSet appNames = getNames(target.getApplications().getValues()); + assertEquals(ImmutableSet.copyOf(names), appNames); + return true; + } + }; + } + + protected void waitForServices(final CloudFoundryBootDashModel target, final String... names) throws Exception { + new ACondition("wait for services to appear", 3000) { + @Override + public boolean test() throws Exception { + ImmutableSet serviceNames = getNames(target.getServices().getValues()); + assertEquals(ImmutableSet.copyOf(names), serviceNames); + return true; + } + }; + } + + protected void waitForElements(final CloudFoundryBootDashModel target, final String... names) throws Exception { + new ACondition("wait for elements to appear", 3000) { + @Override + public boolean test() throws Exception { + ImmutableSet elements = getNames(target.getElements().getValues()); + assertEquals(ImmutableSet.copyOf(names), elements); + return true; + } + }; + } + + private Answer restoreDefaultTemplate() { + return new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + EditTemplateDialogModel dialog = (EditTemplateDialogModel) invocation.getArguments()[0]; + dialog.restoreDefaultsHandler.call(); + dialog.performOk(); + return null; + } + }; + } + + /** + * Create a mockito {@link Answer} that interacts with EditTemplateDialog by setting the template value and then + * clicking the OK button. + */ + private Answer editSetTemplate(final String newText) { + return new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + EditTemplateDialogModel dialog = (EditTemplateDialogModel) invocation.getArguments()[0]; + dialog.template.setValue(newText); + dialog.performOk(); + return null; + } + }; + } + + private void assertSorted(ImmutableList actions) { + String[] actionNames = new String[actions.size()]; + for (int i = 0; i < actionNames.length; i++) { + actionNames[i] = actions.get(i).getText(); + } + + String actual = StringUtils.arrayToDelimitedString(actionNames, "\n"); + + Arrays.sort(actionNames); + String expected = StringUtils.arrayToDelimitedString(actionNames, "\n"); + + assertEquals(expected, actual); + } + + private void debugListener(final String name, ObservableSet set) { + set.addListener(new ValueListener>() { + + @Override + public void gotValue(LiveExpression> exp, ImmutableSet value) { + StringBuilder elements = new StringBuilder(); + for (BootDashElement e : exp.getValue()) { + elements.append(e.getName()); + elements.append(" "); + } + System.out.println(name+" -> "+elements); + } + }); + } + + private void assertNames(ArrayList elements, String... expectNames) { + String[] actualNames = new String[elements.size()]; + for (int i = 0; i < actualNames.length; i++) { + actualNames[i] = elements.get(i).getName(); + } + assertArrayEquals(expectNames, actualNames); + } + + private ImmutableSet getNames(ImmutableSet values) { + Builder builder = ImmutableSet.builder(); + for (BootDashElement e : values) { + builder.add(e.getName()); + } + return builder.build(); + } + + public static void waitForState(final BootDashElement element, final RunState state, long timeOut) throws Exception { + new ACondition("Wait for state "+state, timeOut) { + @Override + public boolean test() throws Exception { + assertEquals(state, element.getRunState()); + return true; + } + }; + } + + private ToggleBootDashModelConnection toggleTargetConnectionAction() { + return actions.getConnectAction(); + } + + private UpdatePasswordAction updatePasswordAction() { + return getInjectedAction(UpdatePasswordAction.class); + } + + private EnableJmxSshTunnelAction enableJmxSshTunnel() { + return getInjectedAction(EnableJmxSshTunnelAction.class); + } + + private SelectManifestAction selectManifestAction() { + return getInjectedAction(SelectManifestAction.class); + } + + private IAction restartOnlyApplicationAction() { + return getInjectedAction(RestartApplicationOnlyAction.class); + } + + @SuppressWarnings("unchecked") + private T getInjectedAction(Class klass) { + return (T) actions.getAllInjectedActions().stream() + .filter(action -> klass.isAssignableFrom(action.getClass())) + .findFirst() + .get(); + } + + /////////////////////////////////////////////////////////////////////////////////// + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryClientTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryClientTest.java new file mode 100644 index 000000000..558eebc36 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryClientTest.java @@ -0,0 +1,1162 @@ +/******************************************************************************* + * 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.springframework.ide.eclipse.boot.dash.test.CloudFoundryTestHarness.APP_DEPLOY_TIMEOUT; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.io.IOUtils; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import org.mockito.Mockito; +import org.osgi.framework.Version; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFAppState; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplicationDetail; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFBuildpack; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFClientParams; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFDomainType; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFServiceInstance; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFSpace; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFStack; +import org.springframework.ide.eclipse.boot.dash.cf.client.HealthChecks; +import org.springframework.ide.eclipse.boot.dash.cf.client.SshClientSupport; +import org.springframework.ide.eclipse.boot.dash.cf.client.SshHost; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFCloudDomainData; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFDomainStatus; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFPushArguments; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.DefaultClientRequestsV2; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.DefaultCloudFoundryClientFactoryV2; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.ReactorUtils; +import org.springframework.ide.eclipse.boot.dash.console.IApplicationLogConsole; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.test.util.SslValidationDisabler; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; +import org.springframework.ide.eclipse.boot.test.util.TestBracketter; +import org.springframework.ide.eclipse.boot.util.RetryUtil; +import org.springframework.ide.eclipse.boot.util.Thunk; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import junit.framework.AssertionFailedError; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class CloudFoundryClientTest { + + private static final String WDC_06_PCF2 = "wdc-06-pcf2-system.oc.vmware.com"; + + public String CFAPPS_IO() { + return get_CFAPPS_IO(clientParams); + } + + public static String get_CFAPPS_IO(CFClientParams clientParams) { + String org = clientParams.getOrgName(); + String api = clientParams.getApiUrl(); + if (org.equals("application-platform-testing")) { + //PWS test space/org + return "cfapps.io"; + } else if (org.equals("pivot-kdevolder")) { + //PEZ + return "cfapps.pez.pivotal.io"; + } else if (api.contains("api.tan.")) { + //TAN + return "tan.springapps.io"; + } else if (api.contains(WDC_06_PCF2)) { + //vmware pws spring/sts-test space + return "wdc-06-pcf2-apps.oc.vmware.com"; + } + throw new AssertionFailedError("unknown test environment, not sure what to expect here"); + } + + public CFCloudDomain[] getExpectedDomains() { + String org = clientParams.getOrgName(); + String api = clientParams.getApiUrl(); + if (org.equals("application-platform-testing")) { + //PWS test space/org + return new CFCloudDomain[] { + new CFCloudDomainData("cfapps.io") + }; + } else if (org.equals("pivot-kdevolder")) { + //PEZ + return new CFCloudDomain[] { + new CFCloudDomainData("cfapps.pez.pivotal.io"), + new CFCloudDomainData("pezapp.io") + }; + } else if (api.contains("api.tan.")) { + //TAN + return new CFCloudDomain[] { + new CFCloudDomainData("tan.springapps.io") + }; + } else if (api.contains(WDC_06_PCF2)) { + //vmware pws spring/sts-test space + return new CFCloudDomain[] { + new CFCloudDomainData("wdc-06-pcf2-apps.oc.vmware.com") + }; + } + throw new AssertionFailedError("unknown test environment, not sure what to expect here"); + } + + public String[] getExectedBuildpacks() { + String org = clientParams.getOrgName(); + String api = clientParams.getApiUrl(); + if (org.equals("application-platform-testing")) { + //PWS test space/org + return new String[] { + "staticfile_buildpack", + "java_buildpack", + "ruby_buildpack" + }; + } else if (org.equals("pivot-kdevolder")) { + //PEZ + return new String[] { + "staticfile_buildpack", + "java_buildpack_offline", + "ruby_buildpack" + }; + } else if (api.contains("api.tan.")) { + //TAN + return new String[] { + "staticfile_buildpack", + "java_buildpack_offline", + "ruby_buildpack" + }; + } else if (api.contains(WDC_06_PCF2)) { + return new String[] { + "staticfile_buildpack", + "java_buildpack_offline", + "ruby_buildpack" + }; + } + throw new AssertionFailedError("unknown test environment, not sure what to expect here"); + } + + public String getExpectedSshHost() { + String org = clientParams.getOrgName(); + String api = clientParams.getApiUrl(); + if (org.equals("application-platform-testing")) { + //PWS + return "ssh.run.pivotal.io"; + } else if (api.contains("api.tan.")) { + //TAN + return "ssh.tan.springapps.io"; + } else if (api.contains(WDC_06_PCF2)) { + return "ssh."+WDC_06_PCF2; + } + throw new AssertionFailedError("unknown test environment, not sure what to expect here"); + } + + public static final Predicate FLAKY_SERVICE_BROKER = (e) -> { + String msg = ExceptionUtil.getMessage(e).toLowerCase(); + return msg.contains("500") + || msg.contains("502") + ; + }; + + private CFClientParams clientParams = CfTestTargetParams.fromEnv(); + private DefaultClientRequestsV2 client = createClient(clientParams); + + public TestBracketter bracketer = new TestBracketter(); + public CloudFoundryServicesHarness services = new CloudFoundryServicesHarness(clientParams, client); + public CloudFoundryApplicationHarness appHarness = new CloudFoundryApplicationHarness(client); + + @Before + public void setup() throws Exception { + SslValidationDisabler.disableSslValidation(); + ReactorUtils.DUMP_STACK_ON_TIMEOUT = true; + } + + @After + public void teardown() throws Exception { + appHarness.dispose(); //apps first because services still bound to apps can't be deleted! + services.dispose(); + if (client!=null) { + client.close(); + } + StsTestUtil.cleanUpProjects(); + ReactorUtils.DUMP_STACK_ON_TIMEOUT = false; + } + + public BootProjectTestHarness projects = new BootProjectTestHarness(ResourcesPlugin.getWorkspace()); + + private UserInteractions ui = Mockito.mock(UserInteractions.class); + + public static DefaultClientRequestsV2 createClient(CFClientParams params) { + try { + DefaultCloudFoundryClientFactoryV2 factory = DefaultCloudFoundryClientFactoryV2.INSTANCE; + return (DefaultClientRequestsV2) factory.getClient(params); + } catch (Exception e) { + throw new Error(e); + } + } + + @Rule + public TestBracketter testBrack = new TestBracketter(); + + @Test + public void testGetApiVersion() throws Exception { + Version version = client.getApiVersion(); + System.out.println("Api version = "+version); + assertNotNull(version); + } + + @Test + public void testGetSpaces() throws Exception { + int success = 0; + int failed = 0; + + Exception error = null; + for (int i = 0; i < 5; i++) { + try { + long start = System.currentTimeMillis(); + List spaces = client.getSpaces(); + long duration = System.currentTimeMillis() - start; + System.out.println("getSpaces -> "+spaces.size()+" spaces in "+ duration + " ms"); + success++; + } catch (Exception e) { + error = e; + failed++; + System.out.println("getSpaces -> "+ExceptionUtil.getMessage(e)); + } + } + System.out.println("getSpaces failure rate = "+failed + "/" +(success+failed)); + if (failed>0) { + throw new IOException("getSpaces failure rate = "+failed + "/" +(success+failed), error); + } + } + + @Test + public void testGetApplicationDetails() throws Exception { + String appName = appHarness.randomAppName(); + + try (CFPushArguments params = new CFPushArguments()) { + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + params.setNoStart(true); + push(params); + } + + { + CFApplicationDetail appDetails = client.getApplication(appName); + assertEquals(0, appDetails.getRunningInstances()); + assertEquals(CFAppState.STOPPED, appDetails.getState()); + assertEquals(ImmutableList.of(), appDetails.getInstanceDetails()); + } + + client.restartApplication(appName, CancelationTokens.NULL); + { + CFApplicationDetail appDetails = client.getApplication(appName); + assertEquals(1, appDetails.getRunningInstances()); + assertEquals(CFAppState.STARTED, appDetails.getState()); + assertEquals(1, appDetails.getInstanceDetails().size()); + } + } + + @Test + public void testPushAndBindServices() throws Exception { + //This test fails occasionally because service binding is 'unreliable'. Had a long discussion + // with Ben Hale. The gist is errors happen and should be expected in distributed world. + //They are coming from 'AppDirect' which manages the services. The errors are mediated through cloudfoundry + // which doesn't knwow how it should handle them. So it passed the buck onto the its callers. + //In this case.... cf-java-client which does the same thing and passes them to us. + //All the reasons why they can't handle these errors also apply to us, which means that + //the operation is simply unreliable and so failure is an expected outcome even when everything + //works correctly. + //To avoid this test case from failing too often we retry it a few times. + RetryUtil.retryTimes("testPushAndBindServices", 4, () -> { + + System.out.println("Executing full test scenario."); + String service1 = services.createTestService(); + String service2 = services.createTestService(); + String service3 = services.createTestService(); //An extra unused service (makes this a better test). + + String appName = appHarness.randomAppName(); + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + params.setServices(ImmutableList.of(service1, service2)); + push(params); + + assertEquals(ImmutableSet.of(service1, service2), getBoundServiceNames(appName)); + + client.bindAndUnbindServices(appName, ImmutableList.of(service1)).block(); + assertEquals(ImmutableSet.of(service1), getBoundServiceNames(appName)); + + client.bindAndUnbindServices(appName, ImmutableList.of(service2)).block(); + assertEquals(ImmutableSet.of(service2), getBoundServiceNames(appName)); + + client.bindAndUnbindServices(appName, ImmutableList.of()).block(); + assertEquals(ImmutableSet.of(), getBoundServiceNames(appName)); + }); + } + + private Set getBoundServiceNames(String appName) throws Exception { + return client.getBoundServicesSet(appName).block(); + } + + @Test + public void testPushAndBindHostAndDomain() throws Exception { + String appName = appHarness.randomAppName(); + + for (int i = 0; i < 2; i++) { + //Why this loop? Because there was bug which CF V2 that made second push fail to bind to + // map a host that was previously mapped. + if (i>0) { + System.out.println("Delete app"); + client.deleteApplication(appName); + } + + System.out.println("Pushing "+(i+1)); + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + params.setRoutes(ImmutableList.of(appName+"."+CFAPPS_IO())); + + push(params); + } + + System.out.println("Pushing SUCCESS"); + + CFApplicationDetail app = client.getApplication(appName); + assertNotNull("Expected application to exist after push: " + appName, app); + + assertEquals(ImmutableSet.of(appName+"."+CFAPPS_IO()), ImmutableSet.copyOf(app.getUris())); + } + + @Test + public void testPushAndBindMultipleHosts() throws Exception { + String[] hostNames = { + appHarness.randomAppName(), + appHarness.randomAppName() + }; + String appName = hostNames[0]; + + CFPushArguments params = new CFPushArguments(); + params.setAppName(hostNames[0]); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + + Set routes = ImmutableSet.copyOf(Stream.of(hostNames) + .map((host) -> host + "." + CFAPPS_IO()) + .collect(Collectors.toList()) + ); + params.setRoutes(routes); + + push(params); + + System.out.println("Pushing SUCCESS"); + + CFApplicationDetail app = client.getApplication(appName); + assertNotNull("Expected application to exist after push: " + appName, app); + + assertEquals(routes, ImmutableSet.copyOf(app.getUris())); + } + + @Test + public void testPushAndSetRoutes() throws Exception { + String[] hostNames = { + appHarness.randomAppName(), + appHarness.randomAppName() + }; + String appName = hostNames[0]; + + CFPushArguments params = new CFPushArguments(); + params.setAppName(hostNames[0]); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + + Set routes = ImmutableSet.copyOf(Stream.of(hostNames) + .map((host) -> host + "." + CFAPPS_IO()) + .collect(Collectors.toList()) + ); + params.setRoutes(routes); + + push(params); + + System.out.println("Pushing SUCCESS"); + + { + CFApplicationDetail app = client.getApplication(appName); + assertNotNull("Expected application to exist after push: " + appName, app); + assertEquals(routes, ImmutableSet.copyOf(app.getUris())); + } + + doSetRoutesTest(appName, ImmutableSet.of(), params.getRandomRoute()); + + for (String route : routes) { + doSetRoutesTest(appName, ImmutableSet.of(route), params.getRandomRoute()); + } + + } + + private void doSetRoutesTest(String appName, ImmutableSet routes, boolean randomRoute) throws Exception { + ReactorUtils.get(client.setRoutes(appName, routes, randomRoute)); + CFApplicationDetail app = client.getApplication(appName); + assertEquals(routes, ImmutableSet.copyOf(app.getUris())); + } + + @Test + public void testPushWithBasicHealthcheckTypes() throws Exception { + //This test is to make sure that hc info is properly passed on by push operation + // to the 'real' cf client. + //Since the push works different on firt push and repush we have to check both! + + String appName = appHarness.randomAppName(); + + String[] HC_TYPES = { + HealthChecks.HC_PORT, + HealthChecks.HC_PROCESS + }; + + for (String hcType : HC_TYPES) { + try (CFPushArguments params = new CFPushArguments()) { + params.setAppName(appName); + params.setRoutes(appName+"."+CFAPPS_IO()); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + params.setHealthCheckType(hcType); + push(params); + CFApplicationDetail app = client.getApplication(appName); + assertNotNull("Expected application to exist after push: " + appName, app); + assertEquals(hcType, app.getHealthCheckType()); + } + } + } + + @Test + public void testPushAndSetHealthcheckHttpEndpoint() throws Exception { + String appName = appHarness.randomAppName(); + + try (CFPushArguments params = new CFPushArguments()) { + params.setAppName(appName); + params.setRoutes(appName+"."+CFAPPS_IO()); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + params.setHealthCheckHttpEndpoint("/test.txt"); + params.setHealthCheckType(HealthChecks.HC_HTTP); + push(params); + CFApplicationDetail app = client.getApplication(appName); + assertNotNull("Expected application to exist after push: " + appName, app); + assertEquals(HealthChecks.HC_HTTP, app.getHealthCheckType()); + assertEquals("/test.txt", app.getHealthCheckHttpEndpoint()); + } + } + + @Test + public void testPushAndSetEnv() throws Exception { + String appName = appHarness.randomAppName(); + + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setRoutes(appName+"."+CFAPPS_IO()); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + params.setEnv(ImmutableMap.of( + "foo", "foo_value", + "bar", "bar_value" + )); + push(params); + + CFApplicationDetail app = client.getApplication(appName); + assertNotNull("Expected application to exist after push: " + appName, app); + ACondition.waitFor("app content to be availabe", 10_000, () -> { + String content = IOUtils.toString(new URI("https://" + appName + '.' + CFAPPS_IO() + "/test.txt")); + assertTrue(content.length() > 0); + assertTrue(content.contains("content")); + }); + + { + Map env = client.getEnv(appName).block(); + assertEquals("foo_value", env.get("foo")); + assertEquals("bar_value", env.get("bar")); + assertEquals(2, env.size()); + } + + client.setEnvVars(appName, ImmutableMap.of("other", "value")).block(); + { + Map env = client.getEnv(appName).block(); + assertEquals("value", env.get("other")); + assertEquals(1, env.size()); + } + + //This last piece is commented because it fails. + //See: https://www.pivotaltracker.com/story/show/116804259 + + // The last var doesn't get removed. Not sure how to fix it. + // But eventually we won't even be using 'setEnvVars' it will be part of the push. + // and its not going to be our problem to fix that. +// client.updateApplicationEnvironment(appName, ImmutableMap.of()).get(); +// { +// Map env = client.getEnv(appName).get(); +// assertEquals(0, env.size()); +// } + } + + @Test + public void testDeleteApplication() throws Exception { + String appName = appHarness.randomAppName(); + + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + push(params); + + CFApplicationDetail app = client.getApplication(appName); + assertTrue(client.applicationExists(appName)); + assertNotNull("Expected application to exist after push: " + appName, app); + + client.deleteApplication(appName); + app = client.getApplication(appName); + assertNull("Expected application to be deleted after delete: " + appName, app); + assertFalse(client.applicationExists(appName)); + } + + @Test + public void testStopApplication() throws Exception { + String appName = appHarness.randomAppName(); + + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + push(params); + + final CFApplicationDetail runningApp = client.getApplication(appName); + assertNotNull("Expected application to exist after push: " + appName, runningApp); + + new ACondition("wait for app '"+ appName +"'to be RUNNING", APP_DEPLOY_TIMEOUT) { + public boolean test() throws Exception { + assertAppRunState(1, runningApp.getRunningInstances(), CFAppState.STARTED, runningApp.getState()); + return true; + } + }; + + client.stopApplication(appName); + final CFApplicationDetail stoppedApp = client.getApplication(appName); + + new ACondition("wait for app '"+ appName +"'to be STOPPED", APP_DEPLOY_TIMEOUT) { + public boolean test() throws Exception { + assertAppRunState(0, stoppedApp.getRunningInstances(), CFAppState.STOPPED, stoppedApp.getState()); + return true; + } + }; + } + + @Test + public void getServices() throws Exception { + RetryUtil.retryWhen("getServices", 5, FLAKY_SERVICE_BROKER, + () -> { + String[] serviceNames = new String[3]; + Set userProvided = new HashSet<>(); + for (int i = 0; i < serviceNames.length; i++) { + if (i%2==0) { + serviceNames[i] = services.createTestService(); + } else { + serviceNames[i] = services.createTestUserProvidedService(); + userProvided.add(serviceNames[i]); + } + } + + List actualServices = client.getServices(); + ImmutableSet actualServiceNames = ImmutableSet.copyOf( + actualServices.stream() + .map(CFServiceInstance::getName) + .collect(Collectors.toList()) + ); + + + for (CFServiceInstance s : actualServices) { + //Note: because these test on CI host run in parallel with others using same space... + // we should assume there might be 'extra stuff' on the space than what we just created here. + //So only check that 'our stuff' exists and looks right and ignore the rest. + String name = s.getName(); + if (ImmutableSet.copyOf(serviceNames).contains(name)) { + System.out.println("Verifying service: "+name); + if (userProvided.contains(name)) { + assertEquals("user-provided", s.getService()); + System.out.println(" user provided => OK"); + } else { + assertEquals(services.getTestServiceAndPlan()[0], s.getService()); + System.out.println(" getService() => OK"); + String expectPlan = services.getTestServiceAndPlan()[1]; + assertEquals(expectPlan, s.getPlan()); + System.out.println(" getPlan() => OK"); + assertIsURL(s.getDashboardUrl()); + System.out.println(" getDashboardUrl() => OK"); + assertIsURL(s.getDocumentationUrl()); + System.out.println(" getDocumentationUrl() => OK"); + assertText(s.getDescription()); + System.out.println(" getDescription() => OK"); + } + } + } + + for (String s : serviceNames) { + assertTrue(s+" not found in "+actualServiceNames, actualServiceNames.contains(s)); + } + } + ); + } + + @Test + public void testServiceCreateAndDelete() throws Exception { + RetryUtil.retryWhen("testServiceCreateAndDelete", 5, FLAKY_SERVICE_BROKER, () -> { + String[] serviceNames = new String[2]; + for (int i = 0; i < serviceNames.length; i++) { + serviceNames[i] = services.randomServiceName(); + }; + for (int i = 0; i < serviceNames.length; i++) { + String serviceName = serviceNames[i]; + if (i%2==0) { + System.out.println("Create service: "+serviceName); + String[] serviceInfo = services.getTestServiceAndPlan(); + client.createService(serviceName, serviceInfo[0], serviceInfo[1]) + .block(CloudFoundryServicesHarness.CREATE_SERVICE_TIMEOUT); + } else { + System.out.println("Create user-provided service: "+serviceName); + client.createUserProvidedService(serviceName, ImmutableMap.of()) + .block(); + } + } + + List services = client.getServices(); + assertServices(services, serviceNames); + for (String serviceName : serviceNames) { + client.deleteServiceAsync(serviceName).block(); + System.out.println("Deleted service: "+serviceName); + } + + assertNoServices(client.getServices(), serviceNames); + }); + } + + @Test + public void testGetBoundServices() throws Exception { + RetryUtil.retryWhen("testGetBoundServices", 5, FLAKY_SERVICE_BROKER, () -> { + String service1 = services.createTestService(); + String service2 = services.createTestService(); + String service3 = services.createTestService(); + + String appName = appHarness.randomAppName(); + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + params.setServices(ImmutableList.of(service1, service2)); + push(params); + + List allApps = client.getApplicationsWithBasicInfo(); + CFApplication app = null; + for (CFApplication a : allApps) { + if (a.getName().equals(appName)) { + app = a; + } + } + assertEquals(ImmutableSet.of(service1, service2), ImmutableSet.copyOf(app.getServices())); + + app = client.getApplication(appName); + assertEquals(ImmutableSet.of(service1, service2), ImmutableSet.copyOf(app.getServices())); + }); + } + + + @Test + public void testGetDomains() throws Exception { + CFClientParams params = CfTestTargetParams.fromEnv(); + client = createClient(params); + List domains = client.getDomains(); + + for (CFCloudDomain d : domains) { + System.out.println(d.getName()+"\t"+d.getType()); + } + + assertContains(domains, getExpectedDomains()); + + assertEquals(CFAPPS_IO(), domains.stream() + .filter(d -> d.getStatus()==CFDomainStatus.SHARED && d.getType()==CFDomainType.HTTP) + .map(d -> d.getName()) + .findFirst() + .orElse(null) + ); + } + + @Test + public void testGetBuildpacks() throws Exception { + client = createClient(CfTestTargetParams.fromEnv()); + List buildpacks = client.getBuildpacks(); + + Set names = Flux.fromIterable(buildpacks) + .map(CFBuildpack::getName) + .collectList() + .map(ImmutableSet::copyOf) + .block(); + + assertContains(names, getExectedBuildpacks()); + } + + @Test + public void testGetStacks() throws Exception { + client = createClient(CfTestTargetParams.fromEnv()); + List stacks = client.getStacks(); + + Set names = Flux.fromIterable(stacks) + .map(CFStack::getName) + .collectList() + .map(ImmutableSet::copyOf) + .block(); + + assertContains(names, + "cflinuxfs3" + ); + } + + @Test + public void testApplicationLogConnection() throws Exception { + client = createClient(CfTestTargetParams.fromEnv()); + + String appName = appHarness.randomAppName(); + IApplicationLogConsole listener = mock(IApplicationLogConsole.class); + Disposable token = client.streamLogs(appName, listener); + assertNotNull(token); + + Future pushResult = doAsync(() -> { + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + push(params); + }); + + ACondition.waitFor("push", TimeUnit.MINUTES.toMillis(4), () -> { + assertTrue(pushResult.isDone()); + }); + pushResult.get(); + + BootDashModelTest.waitForJobsToComplete(); + verify(listener, atLeastOnce()).onMessage(any()); + } + +// @Test +// public void testGetExistingRoutes() throws Exception { +// String appName = "foo";//appHarness.randomAppName(); +// +//// CFPushArguments params = new CFPushArguments(); +//// params.setAppName(appName); +//// params.setApplicationData(getTestZip("testapp")); +//// params.setBuildpack("staticfile_buildpack"); +//// push(params); +// +// for (int i = 0; i < 3; i++) { +// System.out.println("===================="); +// assertTrue(client.getExistingRoutes(appName).toList().get().isEmpty()); +// } +// } + + + + @Test + public void testGetApplicationBuildpack() throws Exception { + String appName = appHarness.randomAppName(); + + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + push(params); + + //Note we try to get the app two different ways because retrieving the info in + // each case is slightly different. + + { + CFApplicationDetail app = client.getApplication(appName); + assertEquals("staticfile_buildpack", app.getBuildpackUrl()); + } + + { + List allApps = client.getApplicationsWithBasicInfo(); + CFApplication app = null; + for (CFApplication a : allApps) { + if (a.getName().equals(appName)) { + app = a; + } + } + assertEquals("staticfile_buildpack", app.getBuildpackUrl()); + } + } + + + @Test + public void testGetApplicationStack() throws Exception { + String appName = appHarness.randomAppName(); + String stackName = "cflinuxfs3"; + + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + params.setStack(stackName); + push(params); + + //Note we try to get the app two different ways because retrieving the info in + // each case is slightly different. + + { + CFApplicationDetail app = client.getApplication(appName); + assertEquals(stackName, app.getStack()); + } + + { + List allApps = client.getApplicationsWithBasicInfo(); + CFApplication app = null; + for (CFApplication a : allApps) { + if (a.getName().equals(appName)) { + app = a; + } + } + assertEquals(stackName, app.getStack()); + } + } + + @Test + public void testGetApplicationTimeout() throws Exception { + String appName = appHarness.randomAppName(); + int timeout = 67; + + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + params.setTimeout(timeout); + push(params); + + //Note we try to get the app two different ways because retrieving the info in + // each case is slightly different. + + { + CFApplicationDetail app = client.getApplication(appName); + assertEquals(timeout, (int)app.getTimeout()); + } + + { + List allApps = client.getApplicationsWithBasicInfo(); + CFApplication app = null; + for (CFApplication a : allApps) { + if (a.getName().equals(appName)) { + app = a; + } + } + assertEquals(timeout, (int)app.getTimeout()); + } + } + + @Test + public void testRandomHost() throws Exception { + //It is now the responsibility of the client to interpret the 'random route' attribute and + // generate random host or port. This test checks if it does that. + String appName = appHarness.randomAppName(); + + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setRoutes(CFAPPS_IO()); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + params.setRandomRoute(true); + push(params); + + CFApplicationDetail app = client.getApplication(appName); + List uris = app.getUris(); + assertEquals(1, uris.size()); + String uri = uris.get(0); + String host = uri.split("\\.")[0]; + String domain = uri.substring(host.length()+1); + assertEquals(CFAPPS_IO(), domain); + assertTrue(StringUtil.hasText(host)); + } + + @Test + public void testGetApplicationCommand() throws Exception { + String appName = appHarness.randomAppName(); + String command = "something interesting"; + + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + params.setCommand(command); + params.setNoStart(true); // Our command is bogus so starting won't work + push(params); + + //Note we try to get the app two different ways because retrieving the info in + // each case is slightly different. + + { + CFApplicationDetail app = client.getApplication(appName); + assertEquals(command, app.getCommand()); + } + + { + List allApps = client.getApplicationsWithBasicInfo(); + CFApplication app = null; + for (CFApplication a : allApps) { + if (a.getName().equals(appName)) { + app = a; + } + } + assertEquals(command, app.getCommand()); + } + } + + @Test public void testSshSupport() throws Exception { + String appName = appHarness.randomAppName(); + + CFPushArguments params = new CFPushArguments(); + params.setAppName(appName); + params.setApplicationData(getTestZip("testapp")); + params.setBuildpack("staticfile_buildpack"); + push(params); + + SshClientSupport sshSupport = client.getSshClientSupport(); + SshHost sshHost = sshSupport.getSshHost(); + System.out.println(sshHost); + assertEquals(getExpectedSshHost(), sshHost.getHost()); + assertEquals(2222, sshHost.getPort()); + assertTrue(StringUtil.hasText(sshHost.getFingerPrint())); + + assertTrue(StringUtil.hasText(sshSupport.getSshCode())); + UUID appGuid = client.getApplication(appName).getGuid(); + String sshUser = sshSupport.getSshUser(appGuid, 0); + System.out.println("sshUser = "+sshUser); + assertTrue(StringUtil.hasText(sshUser)); + + String code = sshSupport.getSshCode(); + System.out.println("sshCode = "+code); + assertTrue(StringUtil.hasText(code)); + } + + @Test public void testGetServiceDashboardUrl() throws Exception { + String serviceName = services.createTestService(); + CFServiceInstance service = null; + for (CFServiceInstance s : client.getServices()) { + if (s.getName().equals(serviceName)) { + service = s; + } + } + String dashUrl = service.getDashboardUrl(); + assertNotNull(dashUrl); + assertTrue(dashUrl.startsWith("https")); + } + + @Test public void startCanBeCanceled() throws Exception { + IProject project = projects.createBootWebProject("slow-starter"); + File jarFile = BootJarPackagingTest.packageAsJar(project, ui); + System.out.println(); + + String appName = appHarness.randomAppName(); + try (CFPushArguments params = new CFPushArguments()) { + params.setAppName(appName); + params.setRoutes(appName+"."+CFAPPS_IO()); + params.setApplicationData(jarFile); + params.setNoStart(true); + + long starting = System.currentTimeMillis(); + System.out.println("Pushing..."); + push(params); + long duration = System.currentTimeMillis() - starting; + System.out.println("Pushing took: "+duration+ " ms"); + } + + CancelationTokens cancelationTokens = new CancelationTokens(); + long starting = System.currentTimeMillis(); + System.out.println("Starting..."); + Future startResult = doAsync(() -> { + client.restartApplication(appName, cancelationTokens.create()); + long duration = System.currentTimeMillis() - starting; + System.out.println("started in "+duration+" ms"); + }); + + Thread.sleep(5000); + long cancelTime = System.currentTimeMillis(); + cancelationTokens.cancelAll(); + try { + startResult.get(5, TimeUnit.SECONDS); + } catch (ExecutionException e) { + e.printStackTrace(); + long duration = System.currentTimeMillis() - cancelTime; + assertEquals(OperationCanceledException.class, ExceptionUtil.getDeepestCause(e).getClass()); + System.out.println("\nRestart Canceled after "+duration+" ms"); + } + } + + @Test public void pushCanBeCanceled() throws Exception { + String appName = appHarness.randomAppName(); + IProject project = projects.createBootWebProject("slow-starter"); + File jarFile = BootJarPackagingTest.packageAsJar(project, ui); + + CancelationTokens cancelationTokens = new CancelationTokens(); + try (CFPushArguments params = new CFPushArguments()) { + params.setAppName(appName); + params.setRoutes(appName+"."+CFAPPS_IO()); + params.setApplicationData(jarFile); + + long starting = System.currentTimeMillis(); + Future pushResult = doAsync(() -> { + System.out.println("Pushing..."); + client.push(params, cancelationTokens.create()); + long duration = System.currentTimeMillis() - starting; + System.out.println("Pushing took: "+duration+ " ms"); + }); + Thread.sleep(Duration.ofSeconds(10).toMillis()); + long cancelTime = System.currentTimeMillis(); + System.out.println("Canceling..."); + cancelationTokens.cancelAll(); + + try { + pushResult.get(5, TimeUnit.SECONDS); // Cancel should happen pretty 'fast'! + fail("push completed but it should have been canceled"); + } catch (ExecutionException e) { // real exception is wrapped in EE by Future.get + e.printStackTrace(); + long duration = System.currentTimeMillis() - cancelTime; + assertEquals(OperationCanceledException.class, ExceptionUtil.getDeepestCause(e).getClass()); + System.out.println("\nPush Canceled after: "+duration +" ms"); + } + } + } + + + ///////////////////////////////////////////////////////////////////////////// + + private void assertText(String s) { + if (!StringUtil.hasText(s)) { + fail("Found no text, but expected some"); + } + } + + private void assertIsURL(String url) throws URISyntaxException { + assertText(url); + new URI(url); //parse it + } + + private Future doAsync(Thunk task) { + CompletableFuture result = new CompletableFuture<>(); + Job job = new Job("Async task") { + protected IStatus run(IProgressMonitor monitor) { + try { + task.call(); + result.complete(null); + } catch (Throwable e) { + result.completeExceptionally(e); + } + return Status.OK_STATUS; + } + }; + job.schedule(); + return result; + } + + private void push(CFPushArguments _params) throws Exception { + if (_params.getMemory() == null) { + _params.setMemory(1024); + } + try (CFPushArguments params = _params) { + client.push(params, CancelationTokens.NULL); + } + } + + private void assertContains(Collection strings, @SuppressWarnings("unchecked") T... expecteds) { + for (T e : expecteds) { + assertContains(e, strings); + } + } + + private void assertContains(T expected, Collection set) { + assertTrue("Expected '"+expected+"' not found in: "+set, set.contains(expected)); + } + + private void assertNoServices(List services, String... serviceNames) throws Exception { + Set names = services.stream().map(CFServiceInstance::getName).collect(Collectors.toSet()); + for (String serviceName : serviceNames) { + assertFalse("Service exists but shouldn't: "+serviceName, names.contains(serviceName)); + } + } + + private void assertServices(List services, String... serviceNames) throws Exception { + Set names = services.stream().map(CFServiceInstance::getName).collect(Collectors.toSet()); + assertContains(names, serviceNames); + } + + private void assertAppRunState(int expectedInstances, int actualInstances, CFAppState expectedRequestedState, CFAppState actualRequestedState) { + assertEquals("Expected running instances does not match actual running instances: ", expectedInstances, actualInstances); + assertEquals("Expected requested app state does not match actual requested app state: ", expectedRequestedState, actualRequestedState); + } + + private File getTestZip(String fileName) { + File sourceWorkspace = new File( + StsTestUtil.getSourceWorkspacePath("org.springframework.ide.eclipse.boot.dash.test")); + File file = new File(sourceWorkspace, fileName + ".zip"); + Assert.isTrue(file.exists(), ""+ file); + return file; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryServicesHarness.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryServicesHarness.java new file mode 100644 index 000000000..7ea4e1230 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryServicesHarness.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.springframework.ide.eclipse.boot.dash.test.CloudFoundryClientTest.FLAKY_SERVICE_BROKER; + +import java.time.Duration; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFClientParams; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.DefaultClientRequestsV2; +import org.springframework.ide.eclipse.boot.util.RetryUtil; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +import com.google.common.collect.ImmutableMap; + +public class CloudFoundryServicesHarness implements Disposable { + + public static final Duration CREATE_SERVICE_TIMEOUT = Duration.ofMinutes(1); + + private static final Duration DELETE_SERVICE_TIMEOUT = Duration.ofMinutes(1); + + private DefaultClientRequestsV2 client; + + private CFClientParams clientParams; + + public CloudFoundryServicesHarness(CFClientParams clientParams, DefaultClientRequestsV2 client) { + this.client = client; + this.clientParams = clientParams; + } + private Set ownedServiceNames = new HashSet<>(); + + public String[] getTestServiceAndPlan() { + String org = clientParams.getOrgName(); + String api = clientParams.getApiUrl(); + if (org.equals("application-platform-testing")) { + //PWS test space/org + return new String[] {"cloudamqp", "lemur"}; + } else if (api.contains("api.run.pez.")) { + //PEZ + return new String[] {"p-rabbitmq", "standard"}; + } else if (api.contains("api.tan.")) { + //TAN RGB + //return new String[] {"app-autoscaler", "bronze"}; //Warning not good enough for some tests! + return new String[] {"p-rabbitmq", "standard"}; + } else if (api.contains("wdc-06-pcf2-system.oc.vmware.com")) { + return new String[] {"p-rabbitmq", "standard"}; + } + return null; + } + + public String randomServiceName() { + String name = StringUtil.datestamp()+"-"+randomAlphabetic(10); + ownedServiceNames.add(name); + return name; + } + + public String createTestUserProvidedService() { + String name = randomServiceName(); + client.createUserProvidedService(name, ImmutableMap.of()).block(CREATE_SERVICE_TIMEOUT); + return name; + } + + public String createTestService() throws Exception { + String name = randomServiceName(); + RetryUtil.retryWhen("createTestService["+name+"]", 5, FLAKY_SERVICE_BROKER, () -> { + String[] serviceParams = getTestServiceAndPlan(); + client.createService(name, serviceParams[0], serviceParams[1]).block(CREATE_SERVICE_TIMEOUT); + }); + return name; + } + + protected void deleteOwnedServices() { + if (!ownedServiceNames.isEmpty()) { + System.out.println("owned services: "+ownedServiceNames); + try { + for (String serviceName : ownedServiceNames) { + System.out.println("delete service: "+serviceName); + try { + RetryUtil.retryTimes("delete sercice "+serviceName, 3, () -> { + this.client.deleteServiceAsync(serviceName).block(DELETE_SERVICE_TIMEOUT); + }); + } catch (Exception e) { + System.out.println("Failed to delete ["+serviceName+"]: "+ExceptionUtil.getMessage(e)); + + // May get 404 or other 400 errors if it is alrready + // gone so don't prevent other owned apps from being + // deleted + } + } + + } catch (Exception e) { + fail("failed to cleanup owned services: " + e.getMessage()); + } + } + } + + @Override + public void dispose() { + assertTrue(!client.isLoggedOut()); + deleteOwnedServices(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryTestHarness.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryTestHarness.java new file mode 100644 index 000000000..b000f57e3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/CloudFoundryTestHarness.java @@ -0,0 +1,387 @@ +/******************************************************************************* + * 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.test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import org.apache.commons.io.IOUtils; +import org.eclipse.core.resources.IFile; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFClientParams; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFSpace; +import org.springframework.ide.eclipse.boot.dash.cf.client.CloudFoundryClientFactory; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.CloudFoundryTargetWizardModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.PasswordDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.StoreCredentialsMode; +import org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.JmxSshTunnelManager; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudFoundryBootDashModel; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTarget; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryRunTargetType; +import org.springframework.ide.eclipse.boot.dash.cf.ui.CfUserInteractions; +import org.springframework.ide.eclipse.boot.dash.dialogs.ManifestDiffDialogModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.LocalBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.SecuredCredentialsStore; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockRunnableContext; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; +import org.springsource.ide.eclipse.commons.frameworks.test.util.Asserter1; + +import com.google.common.collect.ImmutableSet; + +/** + * @author Kris De Volder + */ +public class CloudFoundryTestHarness extends BootDashViewModelHarness { + + @FunctionalInterface + public interface MultipleChoiceAnswerer { + int apply(String title, String msg, String[] choices, int defaultIndex); + } + + public static class DeploymentAnswerer { + String yaml; + String appName; + + public DeploymentAnswerer(String yaml, String appName) { + this.yaml = yaml; + this.appName = appName; + } + + public DeploymentAnswerer(String yaml) { + this(yaml, null); + } + + public DeploymentAnswerer() { + this(null, null); + } + + public void apply(CloudApplicationDeploymentProperties deploymentProperties) throws Exception {}; + } + + @FunctionalInterface + public interface PasswordAnswerer { + void apply(PasswordDialogModel model) throws Exception; + } + + /** + * How long to wait for deleted app to disapear from the model. + */ + public static final long APP_DELETE_TIMEOUT = TimeUnit.MINUTES.toMillis(1); + + /** + * How long to wait for deleted app to disapear from the model. + */ + public static final long SERVICE_DELETE_TIMEOUT = TimeUnit.MINUTES.toMillis(1); + + /** + * How long to wait for a deployed app to show up in the model? This should + * be relatively short. + */ + public static final long APP_IS_VISIBLE_TIMEOUT = 20_000; + + /** + * How long to wait for a deployed app to transition to running state. + */ + public static final long APP_DEPLOY_TIMEOUT = TimeUnit.MINUTES.toMillis(6); + + /** + * How long to wait on retrieving request mappings from a CF app. + */ + public static final long FETCH_REQUEST_MAPPINGS_TIMEOUT = 5_000; + /** + * How long to wait for runtarget to become 'connected'. + */ + public static final long CONNECT_TARGET_TIMEOUT = 30_000; + + public static CloudFoundryTestHarness create(TestBootDashModelContext context) throws Exception { + context.injections.assertDefinitionFor(CloudFoundryClientFactory.class); + context.injections.assertNoDefinitionFor(RunTargetType.class); + context.injections.defInstance(RunTargetType.class, RunTargetTypes.LOCAL); + context.injections.def(CloudFoundryRunTargetType.class, CloudFoundryRunTargetType::new); + return new CloudFoundryTestHarness(context); + } + + + public CloudFoundryBootDashModel getCfTargetModel() { + return (CloudFoundryBootDashModel) getRunTargetModel(cfTargetType()); + } + + private RunTargetType cfTargetType() { + List types = context.injections.getBeans(RunTargetType.class); + for (RunTargetType t : types) { + if (t instanceof CloudFoundryRunTargetType) { + return t; + } + } + throw new NoSuchElementException("No cf target type"); + } + + private CloudFoundryClientFactory clientFactory() { + return context.injections.getBean(CloudFoundryClientFactory.class); + } + + public CloudFoundryBootDashModel createCfTarget( + CFClientParams params, + StoreCredentialsMode storePassword + ) throws Exception { + return createCfTarget(params, storePassword, (wizard) -> assertOk(wizard.getValidator())); + } + + public CloudFoundryBootDashModel createCfTarget( + CFClientParams params, + StoreCredentialsMode storePassword, + Asserter1 wizardAsserter + ) throws Exception { + CloudFoundryTargetWizardModel wizard = new CloudFoundryTargetWizardModel(cfTargetType(), clientFactory(), NO_TARGETS, context); + + wizard.setUrl(params.getApiUrl()); + wizard.setUsername(params.getUsername()); + wizard.setStoreCredentials(storePassword); + wizard.setMethod(params.getCredentials().getType().toLoginMethod()); + wizard.setPassword(params.getCredentials().getSecret()); + wizard.setSelfsigned(params.isSelfsigned()); + wizard.skipSslValidation(params.skipSslValidation()); + wizard.resolveSpaces(new MockRunnableContext()); + assertNotNull(wizard.getRefreshToken()); + wizard.setSpace(getSpace(wizard, params.getOrgName(), params.getSpaceName())); + wizardAsserter.execute(wizard); + final CloudFoundryRunTarget newTarget = wizard.finish(); + if (newTarget!=null) { + model.getRunTargets().add(newTarget); + } + final CloudFoundryBootDashModel targetModel = getCfModelFor(newTarget); + //The created targetModel automatically connected, but this happens asynchly. + new ACondition("Wait for connected state", CONNECT_TARGET_TIMEOUT) { + public boolean test() throws Exception { + return targetModel.isConnected(); + } + }; + return targetModel; + } + + public CloudFoundryBootDashModel createCfTarget(CFClientParams params) throws Exception { + return createCfTarget(params, StoreCredentialsMode.STORE_PASSWORD); + } + + public CloudFoundryBootDashModel getCfModelFor(CloudFoundryRunTarget cfTarget) { + return (CloudFoundryBootDashModel) model.getSectionByTargetId(cfTarget.getId()); + } + + private static final ImmutableSet NO_TARGETS = ImmutableSet.of(); + + private CloudFoundryTestHarness(TestBootDashModelContext context) throws Exception { + super(context); + context.injections.assertDefinitionFor(CloudFoundryRunTargetType.class); + context.injections.assertDefinitionFor(CloudFoundryClientFactory.class); + } + + private static CFSpace getSpace(CloudFoundryTargetWizardModel wizard, String orgName, String spaceName) { + for (CFSpace space : wizard.getSpaces().getOrgSpaces(orgName)) { + if (space.getName().equals(spaceName)) { + return space; + } + } + fail("Not found org/space = "+orgName+"/"+spaceName); + return null; + } + + public void answerDeploymentPrompt(CfUserInteractions ui, final String appName, final String hostName) throws Exception { + final String yaml = "applications:\n" + + "- name: "+appName+"\n" + + " host: "+hostName+"\n" + + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ \"jre\": { version: 11.+ } }'\n"; + answerDeploymentPrompt(ui, new DeploymentAnswerer(yaml)); + } + + public void answerConfirmationMultipleChoice(UserInteractions ui, MultipleChoiceAnswerer answerer) { + doAnswer(new Answer() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + // int confirmOperation(String title, String message, String[] buttonLabels, int defaultButtonIndex); + String title = (String) invocation.getArguments()[0]; + String msg = (String) invocation.getArguments()[1]; + String[] choices = (String[]) invocation.getArguments()[2]; + int defaultIndex = (int) invocation.getArguments()[3]; + return answerer.apply(title, msg, choices, defaultIndex); + } + }) + .when(ui) + .confirmOperation(any(), any(), any(), anyInt()); + } + + + + public void answerDeploymentPrompt(CfUserInteractions ui, final String appName, final String hostName, final List bindServices) throws Exception { + //TODO: replace this method with something more 'generic' that accepts a function which is passed the deploymentProperties + // so that it can add additional infos to it. + final String yaml = "applications:\n" + + "- name: "+appName+"\n" + + " host: "+hostName+"\n" + + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ \"jre\": { version: 11.+ } }'\n" + + createServicesBlock(bindServices); + + answerDeploymentPrompt(ui, new DeploymentAnswerer(yaml)); + } + + private String createServicesBlock(List bindServices) { + if (bindServices==null || bindServices.isEmpty()) { + return ""; + } + StringBuilder buf = new StringBuilder(" services:\n"); + for (String s : bindServices) { + buf.append(" - "+s+"\n"); + } + return buf.toString(); + } + + private String createEnvBlock(Map env) { + if (env==null || env.isEmpty()) { + return " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ \"jre\": { version: 11.+ } }'\n"; + } + StringBuilder buf = new StringBuilder( + " env:\n" + + " JBP_CONFIG_OPEN_JDK_JRE: '{ \"jre\": { version: 11.+ } }'\n" + ); + for (Entry e : env.entrySet()) { + buf.append(" "+e.getKey()+": "+e.getValue()); + } + return buf.toString(); + } + + public void answerDeploymentPrompt(CfUserInteractions ui, final String appName, final String hostName, final Map env) throws Exception { + String yaml = "applications:\n" + + "- name: "+appName+"\n" + + " host: "+hostName+"\n" + + createEnvBlock(env); + answerDeploymentPrompt(ui, new DeploymentAnswerer(yaml)); + } + + /** + * Does the same thing as what would happen if a user answered the deployment props dialog by selecting an + * existing manifest file. + */ + public void answerDeploymentPrompt(CfUserInteractions ui, IFile manifestToSelect) throws Exception { + String yaml = IOUtils.toString(manifestToSelect.getContents(), manifestToSelect.getCharset()); + answerDeploymentPrompt(ui, new DeploymentAnswerer(yaml) { + @Override + public void apply(CloudApplicationDeploymentProperties properties) throws Exception { + properties.setManifestFile(manifestToSelect); + } + }); + + } + + public void answerDeploymentPrompt(CfUserInteractions ui, DeploymentAnswerer answerer) throws Exception { + when(ui.promptApplicationDeploymentProperties(any(DeploymentPropertiesDialogModel.class))) + .thenAnswer(new Answer() { + @Override + public CloudApplicationDeploymentProperties answer(InvocationOnMock invocation) throws Throwable { + DeploymentPropertiesDialogModel dialog = (DeploymentPropertiesDialogModel) invocation.getArguments()[0]; + CloudApplicationDeploymentProperties deploymentProperties = dialog.getDeploymentProperties(answerer.yaml, answerer.appName); + answerer.apply(deploymentProperties); + return deploymentProperties; + } + + }); + } + + public String privateStoreKey(CloudFoundryBootDashModel target) { + return secureStoreKey(target)+":token"; + } + + public String secureStoreKey(CloudFoundryBootDashModel target) { + return target.getRunTarget().getType().getName()+":"+target.getRunTarget().getId(); + } + + public void answerPasswordPrompt(CfUserInteractions ui, PasswordAnswerer answerer) { + doAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + PasswordDialogModel dialog = (PasswordDialogModel) invocation.getArguments()[0]; + answerer.apply(dialog); + return dialog.isOk(); + } + }).when(ui).openPasswordDialog(any(PasswordDialogModel.class)); + } + + public List getCfRunTargetModels() { + return getRunTargetModels(cfTargetType()); + } + + public CloudFoundryRunTargetType getCfTargetType() { + for (RunTargetType type : model.getRunTargetTypes()) { + if (type instanceof CloudFoundryRunTargetType) { + return (CloudFoundryRunTargetType) type; + } + } + return null; + } + + /** + * Raw fetch of environment variables (makes a request through the CF client, rather then get the cached data + * from the model). + */ + public Map fetchEnvironment(CloudFoundryBootDashModel model, String appName) throws Exception { + return model.getRunTarget().getClient().getApplication(appName).getEnvAsMap(); + } + + public LocalBootDashModel getLocalModel() { + return (LocalBootDashModel) getRunTargetModel(RunTargetTypes.LOCAL); + } + + public void answerManifestDiffDialog(CfUserInteractions ui, Function answerer) throws Exception { + when(ui.openManifestDiffDialog(any())) + .thenAnswer(new Answer() { + @Override + public ManifestDiffDialogModel.Result answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + ManifestDiffDialogModel dialog = (ManifestDiffDialogModel) args[0]; + return answerer.apply(dialog); + } + }); + } + + public SecuredCredentialsStore getCredentialsStore() { + return context.getSecuredCredentialsStore(); + } + + public IPropertyStore getPrivateStore() { + return context.getPrivatePropertyStore(); + } + + public JmxSshTunnelManager getJmxSshTunnelManager() { + return context.injections.getBean(JmxSshTunnelManager.class); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/DeploymentPropertiesDialogModelTests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/DeploymentPropertiesDialogModelTests.java new file mode 100644 index 000000000..06ea19f48 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/DeploymentPropertiesDialogModelTests.java @@ -0,0 +1,1237 @@ +/******************************************************************************* + * Copyright (c) 2016, 2020 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.springframework.ide.eclipse.boot.dash.test.BootDashModelTest.waitForJobsToComplete; +import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.createFile; + +import java.io.ByteArrayInputStream; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.eclipse.core.filebuffers.FileBuffers; +import org.eclipse.core.filebuffers.ITextFileBufferManager; +import org.eclipse.core.filebuffers.LocationKind; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.lsp4e.LanguageServerWrapper; +import org.eclipse.lsp4e.LanguageServiceAccessor; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.editors.text.TextFileDocumentProvider; +import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.ui.texteditor.DocumentProviderRegistry; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.mockito.Mockito; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFAppState; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFDomainType; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFCloudDomainData; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFDomainStatus; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudData; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialog; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialogModel; +import org.springframework.ide.eclipse.boot.dash.cf.dialogs.DeploymentPropertiesDialogModel.ManifestType; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; +import org.springframework.ide.eclipse.boot.test.util.TestBracketter; +import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.util.Log; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +import com.google.common.collect.ImmutableList; + +import junit.framework.AssertionFailedError; + + +/** + * Tests for {@link DeploymentPropertiesDialogModel} + * + * @author Alex Boyko + * + */ +@SuppressWarnings("restriction") +public class DeploymentPropertiesDialogModelTests { + + private static final String CF_SERVER_ID = "org.eclipse.languageserver.languages.cloudfoundrymanifest"; + + private static final long DISCONNECT_TIMEOUT = 10000; + + private static final long TIMEOUT = 3000; + + private static final int DONT_SAVE_BUTTON_ID = 1; + + private static final int SAVE_BUTTON_ID = 0; + + private static final String UNKNOWN_DEPLOYMENT_MANIFEST_TYPE_MUST_BE_EITHER_FILE_OR_MANUAL = "Unknown deployment manifest type. Must be either 'File' or 'Manual'."; + private static final String MANIFEST_DOES_NOT_CONTAIN_DEPLOYMENT_PROPERTIES_FOR_APPLICATION_WITH_NAME = "Manifest does not contain deployment properties for application with name ''{0}''."; + private static final String MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED = "Manifest does not have any application defined."; + private static final String ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY = "Enter deployment manifest YAML manually."; + private static final String CURRENT_GENERATED_DEPLOYMENT_MANIFEST = "Current generated deployment manifest."; + private static final String CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM = "Choose an existing deployment manifest YAML file from the local file system."; + private static final String DEPLOYMENT_MANIFEST_FILE_NOT_SELECTED = "Deployment manifest file not selected."; + private static final String MANIFEST_YAML_ERRORS = "Deployment manifest YAML has errors."; + + public static final String DEFAULT_BUILDPACK = "java_buildpack_offline"; + + public static final List SPRING_CLOUD_DOMAINS = Arrays.asList(new CFCloudDomainData("springsource.org", CFDomainType.HTTP, CFDomainStatus.SHARED)); + + public static CloudData createCloudData() { + return new CloudData(SPRING_CLOUD_DOMAINS, DEFAULT_BUILDPACK, ImmutableList.of()); + } + + private static BootProjectTestHarness projects; + private static IProject dumbProject; + private UserInteractions ui; + private DeploymentPropertiesDialogModel model; + + private Shell shell; + + //////////////////////////////////////////////////////////// + + @Rule public TestName name = new TestName(); + @Rule public TestBracketter bracketer = new TestBracketter(); + + @BeforeClass + public static void beforeAll() throws Exception { + StsTestUtil.deleteAllProjects(); + waitForJobsToComplete(); + projects = new BootProjectTestHarness(ResourcesPlugin.getWorkspace()); + dumbProject = projects.createProject("dumbProject"); + IFile manifest = dumbProject.getFile("manifest.yml"); + manifest.create(new ByteArrayInputStream(new byte[0]), true, new NullProgressMonitor()); + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().openEditor(new FileEditorInput(manifest), "CfManifestYMLEditor"); + waitForJobsToComplete(); + waitUntilFileConnected(manifest); + } + + @AfterClass + public static void afterAll() throws Exception { + StsTestUtil.deleteAllProjects(); + waitForJobsToComplete(); + waitForServerShutdown(); + } + + @Before + public void setup() throws Exception { + Log.info("STARTED test: " + name.getMethodName()); + StsTestUtil.deleteAllProjectsExcept(dumbProject.getName()); +// StsTestUtil.deleteAllProjects(); + this.ui = mock(UserInteractions.class); + } + + @After + public void tearDown() throws Exception { + if (shell != null) { + shell.dispose(); + } + waitForJobsToComplete(); + disposeModel(); +// LanguageServiceAccessor.clearStartedServers(); + waitForJobsToComplete(); +// Thread.sleep(3000); + Log.info("FINISHED test: " + name.getMethodName()); + } + + private static void waitForServerShutdown() throws Exception { + ACondition.waitFor("ls servers shutdown", DISCONNECT_TIMEOUT, () -> { + List servers = LanguageServiceAccessor.getActiveLanguageServers(x -> true); + assertTrue("still running server: "+servers, servers.isEmpty()); + }); + } + + private void disposeModel() throws Exception { + if (model != null) { + IEditorInput fileInput = model.getFileYamlEditor().getEditorInput(); + IEditorInput manualInput = model.getManualYamlEditor().getEditorInput(); + model.dispose(); + if (fileInput instanceof IFileEditorInput) { + List servers = getCfLanguageServers(((IFileEditorInput) fileInput).getFile(), null); + if (!servers.isEmpty()) { + waitUntilFileDisconnedted(((IFileEditorInput)fileInput).getFile()); + } + } + if (manualInput instanceof IFileEditorInput) { + List servers = getCfLanguageServers(((IFileEditorInput) manualInput).getFile(), null); + if (!servers.isEmpty()) { + waitUntilFileDisconnedted(((IFileEditorInput)manualInput).getFile()); + } + } + model = null; + waitForJobsToComplete(); + } + } + + private void waitUntilFileDisconnedted(IFile file) throws Exception { + waitForJobsToComplete(); + if (file.exists()) { + ACondition.waitFor(file.toString() + " disconnected from LS", DISCONNECT_TIMEOUT, () -> { + LanguageServerWrapper wrapper = getCfLanguageServer(file); + assertFalse(wrapper.isConnectedTo(file.getLocation())); + }); + } + FileEditorInput editorInput = new FileEditorInput(file); + TextFileDocumentProvider docProvider = (TextFileDocumentProvider) DocumentProviderRegistry.getDefault().getDocumentProvider(editorInput); + assertNull(docProvider.getDocument(editorInput)); + waitForJobsToComplete(); + ITextFileBufferManager textFileBufferManager = FileBuffers.getTextFileBufferManager(); +// textFileBufferManager.disconnect(file.getFullPath(), LocationKind.LOCATION, new NullProgressMonitor()); + assertNull(textFileBufferManager.getFileBuffer(file.getFullPath(), LocationKind.LOCATION)); + waitForJobsToComplete(); + } + + private static void waitUntilFileConnected(IFile file) throws Exception { + waitForJobsToComplete(); + ACondition.waitFor(file.toString() + " connected to LS", DISCONNECT_TIMEOUT, () -> { + LanguageServerWrapper wrapper = getCfLanguageServer(file); + assertTrue(wrapper.isConnectedTo(file.getLocation())); + }); + waitForJobsToComplete(); + } + + private static List getCfLanguageServers(IFile file, StringBuilder available ) throws Exception { + Collection wrappers = LanguageServiceAccessor.getLSWrappers(file, cap -> true); + List found = new ArrayList<>(); + for (LanguageServerWrapper wrapper : wrappers) { + if (CF_SERVER_ID.equals(wrapper.serverDefinition.id)) { + found.add(wrapper); + } + if (available!=null) { + available.append(wrapper.serverDefinition.id+" "); + } + } + return found; + } + + private static LanguageServerWrapper getCfLanguageServer(IFile file) throws Exception { + StringBuilder available = new StringBuilder(); + List found = getCfLanguageServers(file, available); + if (found.isEmpty()) { + throw new NoSuchElementException("No CF language server wrapper found in: [ "+available+"]"); + } else if (found.size()>1) { + throw new AssertionFailedError( + "Found more than one ls: "+ + found.stream() + .map(w -> w.serverDefinition.id) + .collect(Collectors.toList()) + ); + } + return found.get(0); + } + + private void createDialogModel(IProject project, CFApplication deployedApp) throws Exception { + model = new DeploymentPropertiesDialogModel(ui, createCloudData(), project, deployedApp, true); + model.initFileModel(); + model.initManualModel(); + shell = new Shell(PlatformUI.getWorkbench().getDisplay()); + shell.setSize(400, 400); + shell.open(); + try { + model.getFileYamlEditor().setContext(DeploymentPropertiesDialog.CONTEXT_DEPLOYMENT_PROPERTIES_DIALOG); + model.getManualYamlEditor().setContext(DeploymentPropertiesDialog.CONTEXT_DEPLOYMENT_PROPERTIES_DIALOG); + model.getFileYamlEditor().createControl(shell); + model.fileYamlEditorControlCreated(); + model.getManualYamlEditor().createControl(shell); + model.manualYamlEditorControlCreated(); + waitUntilFileConnected(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + waitUntilFileConnected(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_MANUAL_MANIFEST_YML)); + } catch (CoreException e) { + Log.log(e); + } + } + + private static CFApplication createCfApp(String name, int memory) { + CFApplication cfApp = mock(CFApplication.class); + Mockito.when(cfApp.getName()).thenReturn(name); + Mockito.when(cfApp.getMemory()).thenReturn(memory); + Mockito.when(cfApp.getBuildpackUrl()).thenReturn(DEFAULT_BUILDPACK); + Mockito.when(cfApp.getCommand()).thenReturn(null); + Mockito.when(cfApp.getDiskQuota()).thenReturn(DeploymentProperties.DEFAULT_MEMORY); + Mockito.when(cfApp.getEnvAsMap()).thenReturn(Collections.emptyMap()); + Mockito.when(cfApp.getGuid()).thenReturn(UUID.randomUUID()); + Mockito.when(cfApp.getInstances()).thenReturn(DeploymentProperties.DEFAULT_INSTANCES); + Mockito.when(cfApp.getRunningInstances()).thenReturn(DeploymentProperties.DEFAULT_INSTANCES); + Mockito.when(cfApp.getServices()).thenReturn(Collections.emptyList()); + Mockito.when(cfApp.getStack()).thenReturn(null); + Mockito.when(cfApp.getState()).thenReturn(CFAppState.STARTED); + Mockito.when(cfApp.getTimeout()).thenReturn(null); + Mockito.when(cfApp.getUris()).thenReturn(Arrays.asList(new String[] {"myapp." + SPRING_CLOUD_DOMAINS.get(0).getName()})); + return cfApp; + } + + + /** + * Select manifest in the model and wait for cf editor to connect, + */ + private void selectAndWait(IFile manifest) throws Exception { + model.setSelectedManifest(manifest); + waitUntilFileConnected(manifest); + } + + //////////////////////////////////////////////////////////////////// + + @Test public void testNoTypeSelected() throws Exception { + IProject project = projects.createProject("p1"); + createDialogModel(project, null); + + assertFalse(model.isManualManifestType()); + assertFalse(model.isFileManifestType()); + + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(UNKNOWN_DEPLOYMENT_MANIFEST_TYPE_MUST_BE_EITHER_FILE_OR_MANUAL, validationResult.msg); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertTrue(deploymentProperties == null); + } + + @Test public void testManualTypeSelected() throws Exception { + IProject project = projects.createProject("p1"); + createDialogModel(project, null); + model.type.setValue(ManifestType.MANUAL); + + assertTrue(model.isManualManifestType()); + assertFalse(model.isManualManifestReadOnly()); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY, validationResult.msg); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(project.getName(), model.getManualSelectedAppName())); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertNotNull(deploymentProperties); + assertEquals(project.getName(), deploymentProperties.getAppName()); + } + + @Test public void testManualTypeForDeployedApp() throws Exception { + IProject project = projects.createProject("p1"); + CFApplication deployedApp = createCfApp("my-test-app", 512); + + createDialogModel(project, deployedApp); + model.type.setValue(ManifestType.MANUAL); + + assertTrue(model.isManualManifestType()); + assertTrue(model.isManualManifestReadOnly()); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(CURRENT_GENERATED_DEPLOYMENT_MANIFEST, validationResult.msg); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(deployedApp.getName(), model.getManualSelectedAppName())); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertNotNull(deploymentProperties); + assertEquals(deploymentProperties.getAppName(), deployedApp.getName()); + assertEquals(deployedApp.getMemory(), deploymentProperties.getMemory()); + } + + @Test(expected = IllegalStateException.class) + public void testManualTypeManifestTextWhenAppDeployed() throws Exception { + IProject project = projects.createProject("p1"); + CFApplication deployedApp = createCfApp("my-test-app", 512); + + createDialogModel(project, deployedApp); + model.type.setValue(ManifestType.MANUAL); + + assertTrue(model.isManualManifestType()); + assertTrue(model.isManualManifestReadOnly()); + + model.setManualManifest("some text"); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED, validationResult.msg); + assertEquals(IStatus.ERROR, validationResult.status); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(null, model.getManualSelectedAppName())); + } + + @Test public void testManualTypeSetManifestText() throws Exception { + IProject project = projects.createProject("p1"); + createDialogModel(project, null); + model.type.setValue(ManifestType.MANUAL); + + assertTrue(model.isManualManifestType()); + assertFalse(model.isManualManifestReadOnly()); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY, validationResult.msg); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(project.getName(), model.getManualSelectedAppName())); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertNotNull(deploymentProperties); + assertEquals(project.getName(), deploymentProperties.getAppName()); + assertNull(deploymentProperties.getManifestFile()); + + String newText = "Some text"; + model.setManualManifest(newText); + assertEquals(newText, model.getManualDocument().get()); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED, validationResult.msg); + }); + } + + @Test public void testFileManifestFileNotSelected() throws Exception { + IProject project = projects.createProject("p1"); + createDialogModel(project, null); + model.type.setValue(ManifestType.FILE); + // With LS approach there would be annotation model for dumb manifest yaml file + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(DEPLOYMENT_MANIFEST_FILE_NOT_SELECTED, validationResult.msg); + }); + } + + @Test public void testFileManifestNonYamlFileSelected() throws Exception { + IProject project = projects.createProject("p1"); + IFile file = createFile(project, "manifest.yml", "Some text content!"); + createDialogModel(project, null); + model.type.setValue(ManifestType.FILE); + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + + ACondition.waitFor("reconcile to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED, validationResult.msg); + }); + } + + @Test public void testFileManifestFolderSelected() throws Exception { + IProject project = projects.createProject("p1"); + createDialogModel(project, null); + model.type.setValue(ManifestType.FILE); + model.setSelectedManifest(project); + + ACondition.waitFor("reconcile to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(DEPLOYMENT_MANIFEST_FILE_NOT_SELECTED, validationResult.msg); + }); + } + + @Test public void testFileManifestFileSelected() throws Exception { + IProject project = projects.createProject("p1"); + String appNameFromFile = "app-name-from-file"; + IFile file = createFile(project, "manifest.yml", + "applications:\n" + + "- name: " + appNameFromFile + "\n" + + " memory: 512M\n" + ); + createDialogModel(project, null); + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + model.type.setValue(ManifestType.FILE); + + ACondition.waitFor("reconcile to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + System.out.println("DOC: " + model.getFileDocument().get()); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + assertEquals(IStatus.INFO, validationResult.status); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(appNameFromFile, model.getFileSelectedAppName())); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertNotNull(deploymentProperties); + assertEquals(appNameFromFile, deploymentProperties.getAppName()); + IFile manifestFile = deploymentProperties.getManifestFile(); + assertNotNull(manifestFile); + assertEquals("manifest.yml", manifestFile.getName()); + } + + @Test public void testSwitchingManifestTypeAndFiles() throws Exception { + IProject project = projects.createProject("p1"); + String appNameFromFile = "app-name-from-file"; + IFile validFile = createFile(project, "manifest.yml", + "applications:\n" + + "- name: " + appNameFromFile + "\n" + + " memory: 512M\n" + ); + IFile invalidFile = createFile(project, "text.yml", "Some text"); + + createDialogModel(project, null); + model.type.setValue(ManifestType.MANUAL); + waitForJobsToComplete(); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY, validationResult.msg); + }); + + model.type.setValue(ManifestType.FILE); + waitForJobsToComplete(); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(DEPLOYMENT_MANIFEST_FILE_NOT_SELECTED, validationResult.msg); + }); + + selectAndWait(validFile); + waitForJobsToComplete(); + System.out.println("New manifest set"); + System.out.println(model.getFileYamlEditor().getViewer().getDocument().get()); + System.out.println("Before conditional wait"); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + System.out.println(model.getFileYamlEditor().getViewer().getDocument().get()); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + }); + + model.type.setValue(ManifestType.MANUAL); + waitForJobsToComplete(); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY, validationResult.msg); + }); + + model.type.setValue(ManifestType.FILE); + waitForJobsToComplete(); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + }); + + model.setSelectedManifest(project); + waitForJobsToComplete(); + waitUntilFileDisconnedted(validFile); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(DEPLOYMENT_MANIFEST_FILE_NOT_SELECTED, validationResult.msg); + }); + + model.setSelectedManifest(invalidFile); + waitForJobsToComplete(); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED, validationResult.msg); + }); + } + + @Test public void testValidSingleAppFileSelectedForDeployedApp() throws Exception { + String appName = "my-test-app"; + IProject project = projects.createProject("p1"); + CFApplication deployedApp = createCfApp(appName, 512); + + IFile validFileSingleName = createFile(project, "manifest.yml", + "applications:\n" + + "- name: " + appName + "\n" + + " memory: 512M\n" + ); + + createDialogModel(project, deployedApp); + model.type.setValue(ManifestType.FILE); + waitForJobsToComplete(); + + selectAndWait(validFileSingleName); + waitForJobsToComplete(); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + }); + } + + @Test public void testInvalidSingleAppFileSelectedForDeployedApp() throws Exception { + String appName = "my-test-app"; + IProject project = projects.createProject("p1"); + CFApplication deployedApp = createCfApp(appName, 512); + + IFile invalidFileSingleName = createFile(project, "manifest.yml", + "applications:\n" + + "- name: someApp\n" + + " memory: 512M\n" + ); + + createDialogModel(project, deployedApp); + model.type.setValue(ManifestType.FILE); + + selectAndWait(invalidFileSingleName); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(MessageFormat.format(MANIFEST_DOES_NOT_CONTAIN_DEPLOYMENT_PROPERTIES_FOR_APPLICATION_WITH_NAME, appName), validationResult.msg); + }); + } + + @Test public void testValidMultiAppFileSelectedForDeployedApp() throws Exception { + String appName = "my-test-app"; + IProject project = projects.createProject("p1"); + CFApplication deployedApp = createCfApp(appName, 512); + + IFile validFileMultiName = createFile(project, "manifest.yml", + "applications:\n" + + "- name: " + project.getName() + "\n" + + " memory: 512M\n" + + "- name: " + appName + "\n" + + " memory: 512M\n" + + "- name: someApp\n" + + " memory: 512M\n" + ); + + createDialogModel(project, deployedApp); + model.type.setValue(ManifestType.FILE); + + selectAndWait(validFileMultiName); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(deployedApp.getName(), model.getFileSelectedAppName())); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertNotNull(deploymentProperties); + assertEquals(deployedApp.getName(), deploymentProperties.getAppName()); + } + + @Test public void testInvalidMultiAppFileSelectedForDeployedApp() throws Exception { + String appName = "my-test-app"; + IProject project = projects.createProject("p1"); + CFApplication deployedApp = createCfApp(appName, 512); + + IFile invalidFileMultiName = createFile(project, "manifest.yml", + "applications:\n" + + "- name: " + project.getName() + "\n" + + " memory: 512M\n" + + "- name: anotherApp\n" + + " memory: 512M\n" + + "- name: someApp\n" + + " memory: 512M\n" + ); + + createDialogModel(project, deployedApp); + model.type.setValue(ManifestType.FILE); + + selectAndWait(invalidFileMultiName); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(MessageFormat.format(MANIFEST_DOES_NOT_CONTAIN_DEPLOYMENT_PROPERTIES_FOR_APPLICATION_WITH_NAME, appName), validationResult.msg); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(null, model.getFileSelectedAppName())); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertNull(deploymentProperties); + } + + @Test public void testSwitchingWithDeployedApp() throws Exception { + String appName = "my-test-app"; + IProject project = projects.createProject("p1"); + CFApplication deployedApp = createCfApp(appName, 512); + + IFile validFileMultiName = createFile(project, "valid-manifest.yml", + "applications:\n" + + "- name: " + project.getName() + "\n" + + " memory: 512M\n" + + "- name: " + appName + "\n" + + " memory: 512M\n" + + "- name: someApp\n" + + " memory: 512M\n" + ); + + IFile invalidFileMultiName = createFile(project, "invalid-manifest.yml", + "applications:\n" + + "- name: " + project.getName() + "\n" + + " memory: 512M\n" + + "- name: anotherApp\n" + + " memory: 512M\n" + + "- name: someApp\n" + + " memory: 512M\n" + ); + + createDialogModel(project, deployedApp); + + model.type.setValue(ManifestType.FILE); + selectAndWait(validFileMultiName); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(appName, model.getFileSelectedAppName())); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + }); + + selectAndWait(invalidFileMultiName); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(null, model.getFileSelectedAppName())); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(MessageFormat.format(MANIFEST_DOES_NOT_CONTAIN_DEPLOYMENT_PROPERTIES_FOR_APPLICATION_WITH_NAME, appName), validationResult.msg); + }); + + model.setSelectedManifest(project); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(DEPLOYMENT_MANIFEST_FILE_NOT_SELECTED, validationResult.msg); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(null, model.getFileSelectedAppName())); + + model.type.setValue(ManifestType.MANUAL); + assertTrue(model.isManualManifestReadOnly()); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(appName, model.getManualSelectedAppName())); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(CURRENT_GENERATED_DEPLOYMENT_MANIFEST, validationResult.msg); + }); + + selectAndWait(invalidFileMultiName); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(null, model.getFileSelectedAppName())); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(CURRENT_GENERATED_DEPLOYMENT_MANIFEST, validationResult.msg); + }); + + model.type.setValue(ManifestType.FILE); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(MessageFormat.format(MANIFEST_DOES_NOT_CONTAIN_DEPLOYMENT_PROPERTIES_FOR_APPLICATION_WITH_NAME, appName), validationResult.msg); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(null, model.getFileSelectedAppName())); + } + + @Test(expected = OperationCanceledException.class) + public void testCancel() throws Exception { + IProject project = projects.createProject("p1"); + createDialogModel(project, null); + model.type.setValue(ManifestType.MANUAL); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY, validationResult.msg); + }); + + model.cancelPressed(); + + assertTrue(model.isCanceled()); + + model.getDeploymentProperties(); + } + + @Test public void testManifestFileLabel() throws Exception { + IProject project = projects.createProject("p1"); + String appNameFromFile = "app-name-from-file"; + IFile file = createFile(project, "manifest.yml", + "applications:\n" + + "- name: " + appNameFromFile + "\n" + + " memory: 512M\n" + ); + createDialogModel(project, null); + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + model.setManifestType(ManifestType.FILE); + + assertEquals(file.getFullPath().toOSString(), model.getFileLabel().getValue()); + + model.getFileDocument().set("some text"); + + assertEquals(file.getFullPath().toOSString() + "*", model.getFileLabel().getValue()); + } + + @Test public void testDiscardCancelWithDirtyManifestFile() throws Exception { + IProject project = projects.createProject("p1"); + String appNameFromFile = "app-name-from-file"; + String text = "applications:\n" + + "- name: " + appNameFromFile + "\n" + + " memory: 512M\n"; + IFile file = createFile(project, "manifest.yml", text); + createDialogModel(project, null); + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + + model.setManifestType(ManifestType.FILE); + + model.getFileDocument().set("some text"); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED, validationResult.msg); + }); + + Mockito.when(ui.confirmOperation(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.anyInt())).thenReturn(DONT_SAVE_BUTTON_ID); + + model.cancelPressed(); + waitForJobsToComplete(); + + assertTrue(model.isCanceled()); + assertEquals(text, IOUtil.toString(file.getContents())); + + /* + * Test document provider is not connected to the file input anymore + */ +// disposeModel(); +// FileEditorInput editorInput = new FileEditorInput(file); +// TextFileDocumentProvider docProvider = (TextFileDocumentProvider) DocumentProviderRegistry.getDefault().getDocumentProvider(editorInput); +// assertNull(docProvider.getDocument(editorInput)); + } + + @Test public void testErrorsInYaml() throws Exception { + IProject project = projects.createProject("p1"); + String appNameFromFile = "app-name-from-file"; + String text = "applications:\n" + + "- name: " + appNameFromFile + "\n" + + " memory: 512M\n"; + IFile file = createFile(project, "manifest.yml", text); + createDialogModel(project, null); + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + model.setManifestType(ManifestType.FILE); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + }); + + model.getFileDocument().set("applications:\n" + + "- name: " + appNameFromFile + "\n" + + " memory: 512MXX\n"); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(MANIFEST_YAML_ERRORS, validationResult.msg); + }); + + } + + @Test public void testSaveCancelWithDirtyManifestFile() throws Exception { + IProject project = projects.createProject("p1"); + String appNameFromFile = "app-name-from-file"; + String text = "applications:\n" + + "- name: " + appNameFromFile + "\n" + + " memory: 512M\n"; + IFile file = createFile(project, "manifest.yml", text); + createDialogModel(project, null); + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + model.setManifestType(ManifestType.FILE); + + String newText = "some text"; + model.getFileDocument().set(newText); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED, validationResult.msg); + assertEquals(IStatus.ERROR, validationResult.status); + }); + + Mockito.when(ui.confirmOperation(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.anyInt())).thenReturn(SAVE_BUTTON_ID); + + model.cancelPressed(); + + assertTrue(model.isCanceled()); + assertEquals(newText, IOUtil.toString(file.getContents())); + + /* + * Test document provider is not connected to the file input anymore + */ +// disposeModel(); +// FileEditorInput editorInput = new FileEditorInput(file); +// TextFileDocumentProvider docProvider = (TextFileDocumentProvider) DocumentProviderRegistry.getDefault().getDocumentProvider(editorInput); +// assertNull(docProvider.getDocument(editorInput)); + } + + @Test public void testDiscardOkWithDirtyManifestFile() throws Exception { + IProject project = projects.createProject("p1"); + String appNameFromFile = "app-name-from-file"; + int memory = 512; + String text = "applications:\n" + + "- name: " + appNameFromFile + "\n" + + " memory: " + memory + "M\n"; + IFile file = createFile(project, "manifest.yml", text); + createDialogModel(project, null); + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + model.setManifestType(ManifestType.FILE); + + int newMemory = 256; + String newText = "applications:\n" + + "- name: " + appNameFromFile + "\n" + + " memory: " + newMemory + "M\n"; + model.getFileDocument().set(newText); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + assertEquals(IStatus.INFO, validationResult.status); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(appNameFromFile, model.getFileSelectedAppName())); + + Mockito.when(ui.confirmOperation(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.anyInt())).thenReturn(DONT_SAVE_BUTTON_ID); + + model.okPressed(); + + assertFalse(model.isCanceled()); + assertEquals(text, IOUtil.toString(file.getContents())); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertNotNull(deploymentProperties); + assertEquals(appNameFromFile, deploymentProperties.getAppName()); + assertEquals(newMemory, deploymentProperties.getMemory()); + + + /* + * Test document provider is not connected to the file input anymore + */ +// disposeModel(); +// FileEditorInput editorInput = new FileEditorInput(file); +// TextFileDocumentProvider docProvider = (TextFileDocumentProvider) DocumentProviderRegistry.getDefault().getDocumentProvider(editorInput); +// assertNull(docProvider.getDocument(editorInput)); + } + + @Test public void testSaveOkWithDirtyManifestFile() throws Exception { + IProject project = projects.createProject("p1"); + String appName = "app-name-from-file"; + int memory = 512; + String text = "applications:\n" + + "- name: " + appName + "\n" + + " memory: " + memory + "M\n"; + String newAppName = "new-app"; + int newMemory = 768; + String newText = "applications:\n" + + "- name: " + newAppName + "\n" + + " memory: " + newMemory + "M\n"; + + IFile file = createFile(project, "manifest.yml", text); + createDialogModel(project, null); + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + model.setManifestType(ManifestType.FILE); + + model.getFileDocument().set(newText); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + assertEquals(IStatus.INFO, validationResult.status); + }); + + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(newAppName, model.getFileSelectedAppName())); + + Mockito.when(ui.confirmOperation(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.anyInt())).thenReturn(SAVE_BUTTON_ID); + + model.okPressed(); + + assertFalse(model.isCanceled()); + assertEquals(newText, IOUtil.toString(file.getContents())); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertNotNull(deploymentProperties); + assertEquals(newAppName, deploymentProperties.getAppName()); + assertEquals(newMemory, deploymentProperties.getMemory()); + + /* + * Test document provider is not connected to the file input anymore + */ +// disposeModel(); +// FileEditorInput editorInput = new FileEditorInput(file); +// TextFileDocumentProvider docProvider = (TextFileDocumentProvider) DocumentProviderRegistry.getDefault().getDocumentProvider(editorInput); +// assertNull(docProvider.getDocument(editorInput)); + + } + + @Test public void testDiscardOnDirtyManifestFileSwitch() throws Exception { + IProject project = projects.createProject("p1"); + String appNameFromFile = "app-name-from-file"; + int memory = 512; + String text = "applications:\n" + + "- name: " + appNameFromFile + "\n" + + " memory: " + memory + "M\n"; + IFile file = createFile(project, "manifest.yml", text); + createDialogModel(project, null); + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + model.setManifestType(ManifestType.FILE); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + assertEquals(IStatus.INFO, validationResult.status); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(appNameFromFile, model.getFileSelectedAppName())); + + model.getFileDocument().set("some text"); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED, validationResult.msg); + assertEquals(IStatus.ERROR, validationResult.status); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(null, model.getFileSelectedAppName())); + + Mockito.when(ui.confirmOperation(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.anyInt())).thenReturn(DONT_SAVE_BUTTON_ID); + + model.setSelectedManifest(project); + waitUntilFileDisconnedted(file); + + assertEquals(text, IOUtil.toString(file.getContents())); + + selectAndWait(file); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(appNameFromFile, model.getFileSelectedAppName())); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + assertEquals(IStatus.INFO, validationResult.status); + }); + + assertEquals(text, model.getFileDocument().get()); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertNotNull(deploymentProperties); + assertEquals(appNameFromFile, deploymentProperties.getAppName()); + assertEquals(memory, deploymentProperties.getMemory()); + } + + @Test public void testSaveOnDirtyManifestFileSwitch() throws Exception { + IProject project = projects.createProject("p1"); + String appName = "app-name-from-file"; + int memory = 512; + String text = "applications:\n" + + "- name: " + appName + "\n" + + " memory: " + memory + "M\n"; + String newAppName = "new-app"; + int newMemory = 768; + String newText = "applications:\n" + + "- name: " + newAppName + "\n" + + " memory: " + newMemory + "M\n"; + + IFile file = createFile(project, "manifest.yml", text); + createDialogModel(project, null); + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + model.setManifestType(ManifestType.FILE); + + model.getFileDocument().set(newText); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + assertEquals(IStatus.INFO, validationResult.status); + }); + + Mockito.when(ui.confirmOperation(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.anyInt())).thenReturn(SAVE_BUTTON_ID); + + model.setSelectedManifest(project); + waitUntilFileDisconnedted(file); + + assertEquals(newText, IOUtil.toString(file.getContents())); + + selectAndWait(file); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + assertEquals(IStatus.INFO, validationResult.status); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(newAppName, model.getFileSelectedAppName())); + + assertEquals(newText, model.getFileDocument().get()); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertNotNull(deploymentProperties); + assertEquals(newAppName, deploymentProperties.getAppName()); + assertEquals(newMemory, deploymentProperties.getMemory()); + } + + @Test public void testRecoverFromInvalidManifest() throws Exception { + IProject project = projects.createProject("p1"); + String newAppName = "new-app"; + int newMemory = 768; + String newText = "applications:\n" + + "- name: " + newAppName + "\n" + + " memory: " + newMemory + "M\n"; + + IFile file = createFile(project, "manifest.yml", ""); + createDialogModel(project, null); + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + model.setManifestType(ManifestType.FILE); + + model.getFileDocument().set(newText); + assertTrue(model.getFileLabel().getValue().endsWith("*")); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + assertEquals(IStatus.INFO, validationResult.status); + }); + + Mockito.when(ui.confirmOperation(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.anyInt())).thenReturn(DONT_SAVE_BUTTON_ID); + + model.okPressed(); + + assertFalse(model.isCanceled()); + assertEquals("", IOUtil.toString(file.getContents())); + assertTrue(model.getFileLabel().getValue().endsWith("*")); + + + // Input disconnect at this point - needs to be reset as well. + model.setSelectedManifest(null); + waitUntilFileDisconnedted(file); + selectAndWait(file); + model.getFileDocument().set(newText); + assertTrue(model.getFileLabel().getValue().endsWith("*")); + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + assertEquals(IStatus.INFO, validationResult.status); + }); + ACondition.waitFor("app name reconcile", TIMEOUT, () -> assertEquals(newAppName, model.getFileSelectedAppName())); + + Mockito.reset(ui); + Mockito.when(ui.confirmOperation(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.anyInt())).thenReturn(SAVE_BUTTON_ID); + + model.okPressed(); + + assertFalse(model.isCanceled()); + assertEquals(newText, IOUtil.toString(file.getContents())); + + CloudApplicationDeploymentProperties deploymentProperties = model.getDeploymentProperties(); + assertEquals(newAppName, deploymentProperties.getAppName()); + assertEquals(newMemory, deploymentProperties.getMemory()); + + /* + * Test document provider is not connected to the file input anymore + */ +// disposeModel(); +// FileEditorInput editorInput = new FileEditorInput(file); +// TextFileDocumentProvider docProvider = (TextFileDocumentProvider) DocumentProviderRegistry.getDefault().getDocumentProvider(editorInput); +// assertNull(docProvider.getDocument(editorInput)); + + } + + @Test public void testManualManifestYamlError() throws Exception { + IProject project = projects.createProject("p1"); + createDialogModel(project, null); + // Set resource annotation model +// model.setManualResourceAnnotationModel(new AnnotationModel()); + model.type.setValue(ManifestType.MANUAL); + + assertTrue(model.isManualManifestType()); + assertFalse(model.isManualManifestReadOnly()); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY, validationResult.msg); + }); + + Annotation resourceAnnotation = new Annotation(DeploymentPropertiesDialogModel.LSP_ERROR_ANNOTATION_TYPE, false, "Some error"); + model.getManualResourceAnnotationModel().addAnnotation(resourceAnnotation, new Position(0,0)); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(MANIFEST_YAML_ERRORS, validationResult.msg); + }); + + model.getManualResourceAnnotationModel().removeAnnotation(resourceAnnotation); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY, validationResult.msg); + }); + } + + @Test public void testFileManifestYamlError() throws Exception { + IProject project = projects.createProject("p1"); + String appNameFromFile = "app-name-from-file"; + IFile file = createFile(project, "manifest.yml", + "applications:\n" + + "- name: " + appNameFromFile + "\n" + + " memory: 512M\n" + ); + createDialogModel(project, null); + // Set resource annotation model + selectAndWait(file); + waitUntilFileDisconnedted(project.getFolder(".settings").getFile(DeploymentPropertiesDialogModel.DUMMY_FILE_MANIFEST_YML)); + model.type.setValue(ManifestType.FILE); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + }); + + Annotation resourceAnnotation = new Annotation(DeploymentPropertiesDialogModel.LSP_ERROR_ANNOTATION_TYPE, false, "Some error"); + model.getFileResourceAnnotationModel().addAnnotation(resourceAnnotation, new Position(0,0)); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.ERROR, validationResult.status); + assertEquals(MANIFEST_YAML_ERRORS, validationResult.msg); + }); + + model.getFileResourceAnnotationModel().removeAnnotation(resourceAnnotation); + + ACondition.waitFor("validation to occur", TIMEOUT, () -> { + ValidationResult validationResult = model.getValidator().getValue(); + assertEquals(IStatus.INFO, validationResult.status); + assertEquals(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM, validationResult.msg); + }); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/DumpBootProcessOutput.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/DumpBootProcessOutput.java new file mode 100644 index 000000000..80c410588 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/DumpBootProcessOutput.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import org.springframework.ide.eclipse.boot.launch.process.BootProcessFactory; + +/** + * JUnit 4 style test 'rule' that enables dumping of boot process + * output onto System.out, for the duration of the test. + * + * @author Kris De Volder + */ +public class DumpBootProcessOutput implements TestRule { + + @Override + public Statement apply(final Statement base, final Description description) { + return new Statement() { + public void evaluate() throws Throwable { + BootProcessFactory.ENABLE_OUTPUT_DUMPING = true; + try { + base.evaluate(); + } finally { + BootProcessFactory.ENABLE_OUTPUT_DUMPING = false; + } + } + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/JLRMethodParserTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/JLRMethodParserTest.java new file mode 100644 index 000000000..ce9b97139 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/JLRMethodParserTest.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test; + +import org.springframework.ide.eclipse.boot.dash.model.actuator.JLRMethodParser; + +import junit.framework.TestCase; + +public class JLRMethodParserTest extends TestCase { + + private static String getFQClassName(String data) { + return JLRMethodParser.parseFQClassName(data); + } + + private static String getMethodName(String data) { + return JLRMethodParser.parseMethodName(data); + } + + public void testCase1() throws Exception { + String data = "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(java.security.Principal)"; + assertEquals("org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint", getFQClassName(data)); + assertEquals("invoke", getMethodName(data)); + } + + public void testCase1b() throws Exception { + String data = "public synchronized java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(java.security.Principal)"; + assertEquals("org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint", getFQClassName(data)); + assertEquals("invoke", getMethodName(data)); + } + + public void testCase2() throws Exception { + String data = "java.util.Collection demo.ReservationRestController.reservations()"; + assertEquals("demo.ReservationRestController", getFQClassName(data)); + assertEquals("reservations", getMethodName(data)); + } + + + public void testCase3() throws Exception { + String data = "public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)"; + assertEquals("org.springframework.boot.autoconfigure.web.BasicErrorController", getFQClassName(data)); + assertEquals("error", getMethodName(data)); + } + + public void testCase4() throws Exception { + String data = "public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest) throws java.lang.Exception"; + assertEquals("org.springframework.boot.autoconfigure.web.BasicErrorController", getFQClassName(data)); + assertEquals("error", getMethodName(data)); + } + + public void testGarbage() throws Exception { + assertNull(getFQClassName(null)); + assertNull(getFQClassName("")); + assertNull(getFQClassName("haha")); + assertNull(getFQClassName("String haha()")); + assertNull(getFQClassName("public synchronized String haha()")); + + assertNull(getMethodName(null)); + assertNull(getMethodName("")); + assertNull(getMethodName("haha")); + assertNull(getMethodName("String haha()")); + assertNull(getMethodName("public synchronized String haha()")); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/JarNameGeneratorTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/JarNameGeneratorTest.java new file mode 100644 index 000000000..9f52ddee8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/JarNameGeneratorTest.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test; + +import java.io.File; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.JavaCore; +import org.springframework.ide.eclipse.boot.dash.cf.packaging.JarNameGenerator; +import org.springframework.ide.eclipse.boot.util.JavaProjectUtil; +import org.springsource.ide.eclipse.commons.tests.util.StsTestCase; + +public class JarNameGeneratorTest extends StsTestCase { + + private JarNameGenerator ng = new JarNameGenerator(); + + public void testNamesForJars() { + + assertName("foo-1.2.3.jar", "/home/someplace/foo-1.2.3.jar"); + assertName("foo-1.2.3-1.jar", "/home/some-other-place/foo-1.2.3.jar"); + assertName("foo-1.2.3-2.jar", "/home/and-yet-another/foo-1.2.3.jar"); + + //counters are per-name not per JarNameGenerator instance? + assertName("bar.jar", "/home/bar.jar"); + assertName("bar-1.jar", "/away/bar.jar"); + + //Tolerate '.JAR' in upper case? + + assertName("case-nutter.jar", "/somewhere/case-nutter.JAR"); + assertName("case-nutter-1.jar", "/elsewhere/case-nutter.JAR"); + } + + public void testNamesForProjectFolders() throws Exception { + IProject p = createPredefinedProject("demo-lib"); + IContainer _outputFolder = JavaProjectUtil.getDefaultOutputFolder(JavaCore.create(p)); + File outputFolder = _outputFolder.getLocation().toFile(); + + assertName("demo-lib.jar", outputFolder); + assertName("demo-lib-1.jar", outputFolder); + assertName("demo-lib-2.jar", outputFolder); + } + + public void testNamesForNestedProjectFolders() throws Exception { + IProject p = createPredefinedProject("demo-lib"); + + String nestedName = "nested"; + IProject nested = createGeneralProject(p, nestedName, p.getLocation().append(nestedName)); + + File dep = nested.getLocation().toFile(); + + //The next bit is to check that this is actually a good test. I.e. + // We do in fact have a situation where a nested project implies that several resources in the + // workspace represent the same folder on disk. + IContainer[] containers = getWSRoot().findContainersForLocationURI(dep.toURI()); + assertEquals(2, containers.length); + + //Now check that it makes the right choice and names the jar after 'nested' not after 'demo-lib'. + assertName("nested.jar", dep); + assertName("nested-1.jar", dep); + assertName("nested-2.jar", dep); + } + + protected IProject createGeneralProject(IProject p, String name, IPath loc) throws CoreException { + IProjectDescription desc = getWorkspace().newProjectDescription(name); + IProject nested = getProject(name); + desc.setLocation(loc); + nested.create(desc, new NullProgressMonitor()); + return nested; + } + + private IProject getProject(String name) { + return getWSRoot().getProject(name); + } + + protected IWorkspaceRoot getWSRoot() { + return getWorkspace().getRoot(); + } + + protected IWorkspace getWorkspace() { + return ResourcesPlugin.getWorkspace(); + } + + private void assertName(String expectedName, File dep) { + assertEquals(expectedName, ng.createName(dep)); + } + + private void assertName(String expectedName, String depPath) { + assertName(expectedName, new File(depPath)); + } + + @Override + protected String getBundleName() { + return BootDashTestBundleConstants.BUNDLE_ID; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/JmxSupportTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/JmxSupportTest.java new file mode 100644 index 000000000..f2d2aac71 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/JmxSupportTest.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2018 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.test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshTunnelFactory; +import org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.JmxSshTunnelManager; +import org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.JmxSupport; +import org.springframework.ide.eclipse.boot.dash.cf.model.CloudAppDashElement; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockSshTunnel; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +public class JmxSupportTest { + + CloudAppDashElement cde = mock(CloudAppDashElement.class); + JmxSshTunnelManager tunnels = new JmxSshTunnelManager(); + SshTunnelFactory tunnelFactory = MockSshTunnel::new; + + @Test + public void setupEnvVars() throws Exception { + int testPort = 1234; + when(cde.getBaseRunStateExp()).thenReturn(LiveExpression.constant(RunState.INACTIVE)); + JmxSupport jmx = new JmxSupport(cde, tunnels, tunnelFactory) { + public int getPort() {return testPort; } + }; + + Map env = new HashMap<>(); + jmx.setupEnvVars(env); + + assertEquals( + "-Dcom.sun.management.jmxremote.ssl=false " + + "-Dcom.sun.management.jmxremote.authenticate=false " + + "-Dcom.sun.management.jmxremote.port=1234 " + + "-Dcom.sun.management.jmxremote.rmi.port=1234 " + + "-Djava.rmi.server.hostname=localhost " + + "-Dcom.sun.management.jmxremote.local.only=false " + + "-Dspring.jmx.enabled=true", + env.get("JAVA_OPTS")); + + jmx.setupEnvVars(env); // should erase old and recreate + assertEquals( + "-Dcom.sun.management.jmxremote.ssl=false " + + "-Dcom.sun.management.jmxremote.authenticate=false " + + "-Dcom.sun.management.jmxremote.port=1234 " + + "-Dcom.sun.management.jmxremote.rmi.port=1234 " + + "-Djava.rmi.server.hostname=localhost " + + "-Dcom.sun.management.jmxremote.local.only=false " + + "-Dspring.jmx.enabled=true", + env.get("JAVA_OPTS")); + } + + @Test + public void setupEnvVars_preserve_unrelated_java_opts() throws Exception { + int testPort = 1234; + when(cde.getBaseRunStateExp()).thenReturn(LiveExpression.constant(RunState.INACTIVE)); + JmxSupport jmx = new JmxSupport(cde, tunnels, tunnelFactory) { + public int getPort() {return testPort; } + }; + + Map env = new HashMap<>(); + env.put("JAVA_OPTS", "-Dsomething.already=here"); + jmx.setupEnvVars(env); + + assertEquals("-Dsomething.already=here " + + "-Dcom.sun.management.jmxremote.ssl=false " + + "-Dcom.sun.management.jmxremote.authenticate=false " + + "-Dcom.sun.management.jmxremote.port=1234 " + + "-Dcom.sun.management.jmxremote.rmi.port=1234 " + + "-Djava.rmi.server.hostname=localhost " + + "-Dcom.sun.management.jmxremote.local.only=false " + + "-Dspring.jmx.enabled=true", + env.get("JAVA_OPTS")); + + jmx.setupEnvVars(env); // should erase old and recreate + assertEquals("-Dsomething.already=here " + + "-Dcom.sun.management.jmxremote.ssl=false " + + "-Dcom.sun.management.jmxremote.authenticate=false " + + "-Dcom.sun.management.jmxremote.port=1234 " + + "-Dcom.sun.management.jmxremote.rmi.port=1234 " + + "-Djava.rmi.server.hostname=localhost " + + "-Dcom.sun.management.jmxremote.local.only=false " + + "-Dspring.jmx.enabled=true", + env.get("JAVA_OPTS")); + + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/LaunchCleanups.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/LaunchCleanups.java new file mode 100644 index 000000000..f62c54d88 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/LaunchCleanups.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test; + +import static org.junit.Assert.fail; + +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchManager; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * JUnit 4 style test 'rule' that does some cleanups for test code + * that creates launch configurations and launches processes with them. + * + * @author Kris De Volder + */ +public class LaunchCleanups implements TestRule { + + @Override + public Statement apply(final Statement base, final Description description) { + return new Statement() { + public void evaluate() throws Throwable { + try { + base.evaluate(); + } finally { + cleanups(); + } + } + + }; + } + + private void cleanups() throws Exception { + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunch[] launches = launchManager.getLaunches(); + for (ILaunch l : launches) { + if (!l.isTerminated()) { + fail("Leaky test code leaves launch running? "+nicerToString(l)); + } + launchManager.removeLaunch(l); + } + + for (ILaunchConfiguration conf : launchManager.getLaunchConfigurations()) { + conf.delete(); + } + } + + private String nicerToString(ILaunch l) { + ILaunchConfiguration c = l.getLaunchConfiguration(); + if (c!=null) { + return c.getName(); + } + return l.toString(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/ListenerLeakDetector.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/ListenerLeakDetector.java new file mode 100644 index 000000000..501ffa928 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/ListenerLeakDetector.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import junit.framework.AssertionFailedError; + +import org.eclipse.core.internal.events.NotificationManager; +import org.eclipse.core.internal.events.ResourceChangeListenerList; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.ListenerList; +import org.eclipse.debug.core.DebugPlugin; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; + +/** + * JUnit 4 'Rule' that checks whether a test registered some + * listeners but didn't deregister them. This is often an indication + * of resource/memory leak. + * + * @author Kris De Volder + */ +@SuppressWarnings("restriction") +public class ListenerLeakDetector implements TestRule { + private Set startingListeners; + + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + start(); + base.evaluate(); + verify(); + } + }; + } + + protected void start() throws Exception { + startingListeners = getListeners(); + } + + @SuppressWarnings("unchecked") + protected Set getListeners() throws Exception { + Set listeners = new HashSet<>(); + listeners.addAll(getDebugListeners()); + listeners.addAll(getWorkspaceListeners()); + return listeners; + } + + protected List getDebugListeners() throws Exception { + DebugPlugin plugin = DebugPlugin.getDefault(); + Field f = DebugPlugin.class.getDeclaredField("fEventListeners"); + f.setAccessible(true); + ListenerList list = (ListenerList)f.get(plugin); + List listeners = new ArrayList<>(); + for (Object l : list.getListeners()) { + if (isInteresting(l)) { + listeners.add(l); + } + } + return listeners; + } + + protected List getWorkspaceListeners() throws Exception { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + NotificationManager notMan = (NotificationManager) getField(workspace, "notificationManager"); + ResourceChangeListenerList listenerList = (ResourceChangeListenerList)getField(notMan, "listeners"); + Object[] entries = listenerList.getListeners(); //Watch out, these entries aren't the actual + // listeners yet. + if (entries!=null) { + List listeners = new ArrayList<>(entries.length); + for (int i = 0; i < entries.length; i++) { + Object l = getField(entries[i], "listener"); + if (isInteresting(l)) { + listeners.add(l); + } + } + return listeners; + } + return Collections.emptyList(); + } + + /** + * Eclipse is one noisy bugger when it comes to adding workspace listeners. We really don't care + * about the listener classes we don't own/control for this leak detection... so... + */ + protected boolean isInteresting(Object l) { + String classname = l.getClass().getName(); + return classname.startsWith("org.springframework.ide.eclipse.boot") && !isIgnoredListenerClassName(classname); + } + + private boolean isIgnoredListenerClassName(String classname) { + //Phill's plugin attaches listeners when console ui kicks in. Ignore those, they are only disposed if someone closes / disposes the + // ui associated with the console. + return classname.startsWith("org.springframework.ide.eclipse.boot.restart.RestartConsolePageParticipant"); + } + + private Object getField(Object self, String name) throws Exception { + Field f = self.getClass().getDeclaredField(name); + f.setAccessible(true); + return f.get(self); + } + + protected void verify() throws Throwable { + ACondition.waitFor("listeners removed", 2000, () -> { + Set endingListeners = getListeners(); + for (Object l : endingListeners) { + if (!startingListeners.contains(l)) { + throw new AssertionFailedError("Leaked listener: "+l+" of class "+l.getClass().getName()); + } + } + }); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/OrderBasedComparatorTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/OrderBasedComparatorTest.java new file mode 100644 index 000000000..97f47895c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/OrderBasedComparatorTest.java @@ -0,0 +1,58 @@ +package org.springframework.ide.eclipse.boot.dash.test; + +import static java.lang.Integer.signum; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Comparator; + +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.util.OrderBasedComparator; + +public class OrderBasedComparatorTest { + + Comparator comparator = new OrderBasedComparator("foo", "bar", "zor"); + + private void assertCompare(int expect, String a, String b) { + assertEquals(expect, signum(comparator.compare(a, b))); + } + + @Test + public void testValidCompares() throws Exception { + assertCompare( 0, "foo", "foo"); + assertCompare(-1, "foo", "bar"); + assertCompare(-1, "foo", "zor"); + + assertCompare(+1, "bar", "foo"); + assertCompare( 0, "bar", "bar"); + assertCompare(-1, "bar", "zor"); + + assertCompare(+1, "zor", "foo"); + assertCompare(+1, "zor", "bar"); + assertCompare( 0, "zor", "zor"); + } + + @Test + public void testSorting() throws Exception { + String[] elements = {"bar", "foo", "zor"}; + String[] sortedElements = {"foo", "bar", "zor"}; + + assertTrue(elements!=sortedElements); // if this is not the case test will pass even if comparator + // causes incorrect sorting! + Arrays.sort(elements, comparator); + assertArrayEquals(sortedElements, elements); + } + + @Test(expected=IllegalArgumentException.class) + public void invalidCompare1() throws Exception { + comparator.compare("foo", "BAD"); + } + + @Test(expected=IllegalArgumentException.class) + public void invalidCompare2() throws Exception { + comparator.compare("BAD", "foo"); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/PropertyFileStoreTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/PropertyFileStoreTest.java new file mode 100644 index 000000000..7cbab3ab7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/PropertyFileStoreTest.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.EnumSet; +import java.util.Random; +import java.util.Set; + +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyFileStore; +import org.springsource.ide.eclipse.commons.core.util.OsUtils; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; + +public class PropertyFileStoreTest { + + static Random rnd = new Random(); + + File file; + + @Before + public void setup() { + file = new File("test-"+rnd.nextInt(10000000)+".properties"); + } + + @After + public void tearDown() { + FileUtils.deleteQuietly(file); + } + + @Test + public void basicStoreAndReload() throws Exception { + { + PropertyFileStore store = new PropertyFileStore(file); + assertTrue(store.isEmpty()); + + store.put("test", "something"); + store.put("foo", "bar"); + assertFalse(store.isEmpty()); + + assertProperties(store, + "test", "something", + "foo", "bar" + ); + + store.sync(); + assertPermissions(file); + } + + { + PropertyFileStore store = new PropertyFileStore(file); + + assertProperties(store, + "test", "something", + "foo", "bar" + ); + } + } + + @Test + public void putNullValues() throws Exception { + //Putting null values is supposed to be equivalent to removing a property. + { + PropertyFileStore store = new PropertyFileStore(file); + assertTrue(store.isEmpty()); + + store.put("test", "something"); + store.put("foo", "bar"); + assertFalse(store.isEmpty()); + + assertProperties(store, + "test", "something", + "foo", "bar" + ); + + store.put("test", null); + + assertProperties(store, + "foo", "bar" + ); + + store.sync(); //Before reloading it we must be sure all is saved. + // Store saving is automatic, but not synchronous. + } + + { + PropertyFileStore store = new PropertyFileStore(file); + + assertProperties(store, + "foo", "bar" + ); + } + } + + private void assertProperties(PropertyFileStore store, String... propsAndValues) { + assertTrue("Numer of props and value must be even (alternating prop and value)",propsAndValues.length%2 == 0); + Builder builder = ImmutableMap.builder(); + String p=null, v; + for (int i = 0; i < propsAndValues.length; i++) { + if (i%2==0) { + p = propsAndValues[i]; + } else { + v = propsAndValues[i]; + builder.put(p, v); + } + } + ImmutableMap expected = builder.build(); + assertEquals(expected, store.asMap()); + } + + private static void assertPermissions(File file) throws IOException { + if (OsUtils.isWindows()) { + //skip windows for now + } else { + Path path = file.toPath(); + Set perms = Files.getPosixFilePermissions(path); + assertEquals(EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), perms); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/RecordingElementChangedListener.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/RecordingElementChangedListener.java new file mode 100644 index 000000000..f3e7b9e65 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/RecordingElementChangedListener.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 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.test; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.RunState; + +/** + * @author Martin Lippert + */ +public class RecordingElementChangedListener implements BootDashModel.ElementStateListener { + + private List recordedRunStates = new ArrayList<>(); + + @Override + public void stateChanged(BootDashElement e) { + recordedRunStates.add(e.getRunState()); + } + + public List getRecordedRunStates() { + return recordedRunStates; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/RouteBuilderTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/RouteBuilderTest.java new file mode 100644 index 000000000..44df7afb3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/RouteBuilderTest.java @@ -0,0 +1,312 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test; + +import static org.junit.Assert.assertEquals; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFDomainType; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFCloudDomainData; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFDomainStatus; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudData; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.YamlGraphDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.routes.RouteAttributes; +import org.springframework.ide.eclipse.boot.dash.cf.routes.RouteBinding; +import org.springframework.ide.eclipse.boot.dash.cf.routes.RouteBuilder; +import org.springframework.ide.eclipse.editor.support.yaml.ast.NodeUtil; +import org.springframework.ide.eclipse.editor.support.yaml.ast.YamlASTProvider; +import org.springframework.ide.eclipse.editor.support.yaml.ast.YamlFileAST; +import org.springframework.ide.eclipse.editor.support.yaml.path.YamlPath; +import org.springframework.ide.eclipse.editor.support.yaml.path.YamlTraversal; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.nodes.Node; + +import com.google.common.collect.ImmutableList; + +public class RouteBuilderTest { + + List TEST_DOMAINS = ImmutableList.of( + new CFCloudDomainData("tcp.cfapps.io", CFDomainType.TCP, CFDomainStatus.SHARED), + new CFCloudDomainData("testing.me", CFDomainType.HTTP, CFDomainStatus.OWNED), + new CFCloudDomainData("cfapps.io") + ); + + @Test + public void appNameOnly() throws Exception { + assertRoutes( + "applications:\n" + + "- name: my-app\n" + , //=> + "my-app@cfapps.io" + ); + } + + @Test + public void domainAndHost() throws Exception { + assertRoutes( + "applications:\n" + + "- name: my-app\n" + + " domain: testing.me\n" + + " host: some-host" + , //=> + "some-host@testing.me" + ); + } + + @Test + public void domainOnly() throws Exception { + assertRoutes( + "applications:\n" + + "- name: my-app\n" + + " domain: testing.me\n" + , //=> + "my-app@testing.me" + ); + } + + @Test + public void domainAndNoHost() throws Exception { + assertRoutes( + "applications:\n" + + "- name: my-app\n" + + " domain: testing.me\n" + + " no-hostname: true" + , //=> + "testing.me" + ); + } + + @Test + public void noHostOnly() throws Exception { + assertRoutes( + "applications:\n" + + "- name: my-app\n" + + " no-hostname: true" + , //=> + "cfapps.io" + ); + } + + @Test public void noRoute() throws Exception { + assertRoutes( + "applications:\n" + + "- name: foo\n" + + " no-route: true" + // ==> + /* NONE */ + ); + } + + @Test + public void multipleDomainsAndHosts() throws Exception { + assertRoutes( + "applications:\n" + + "- name: my-app\n" + + " domains: [testing.me, cfapps.io]\n" + + " hosts: [foo, bar]\n" + , // ==> + "foo@testing.me", + "foo@cfapps.io", + "bar@testing.me", + "bar@cfapps.io" + ); + } + + @Test + public void randomRouteNoDomain() throws Exception { + assertRoutes( + "applications:\n" + + "- name: my-app\n" + + " random-route: true\n" + , + "?@cfapps.io" + ); + } + + @Test + public void randomRouteHttpDomain() throws Exception { + assertRoutes( + "applications:\n" + + "- name: my-app\n" + + " random-route: true\n" + + " domain: testing.me\n" + , + "?@testing.me" + ); + } + @Test + public void randomRouteTcpDomain() throws Exception { + assertRoutes( + "applications:\n" + + "- name: my-app\n" + + " random-route: true\n" + + " domain: tcp.cfapps.io\n" + , + "tcp.cfapps.io:?" + ); + } + + + @Test + public void variousUriRoutes() throws Exception { + assertRoutes( + "applications:\n" + + "- name: my-app\n" + + " routes:\n" + + " - route: foo.cfapps.io/somepath\n" + + " - route: cfapps.io\n" + + " - route: tcp.cfapps.io:63111\n" + + " - route: bar.testing.me\n" + + " - route: ambiguous.tcp.cfapps.io\n" + , // => + "foo@cfapps.io/somepath", + "cfapps.io", + "tcp.cfapps.io:63111", + "bar@testing.me", + "ambiguous@tcp.cfapps.io" + ); + } + + @Test + public void noCuttingInTheMiddleOfAWord() throws Exception { + { + List domains = ImmutableList.of( + new CFCloudDomainData("irrellevant.com"), + new CFCloudDomainData("thing.com"), + new CFCloudDomainData("something.com") + ); + assertRoutes(domains, + "applications:\n" + + "- name: my-app\n" + + " routes:\n" + + " - route: foo.something.com\n" + , // ==> + "foo@something.com" + ); + } + { + List domains = ImmutableList.of( + new CFCloudDomainData("irrellevant.com"), + new CFCloudDomainData("thing.com"), + new CFCloudDomainData("com") + ); + assertRoutes(domains, + "applications:\n" + + "- name: my-app\n" + + " routes:\n" + + " - route: foo.something.com\n" + , // ==> + "foo.something@com" + ); + } + } + + /////// Some edge cases below. Their behavior isn't really that important, but the tests + // just here to formally cover the corresponding branches in the code, and make sure + // they don't behave irrationally or crash. These tests are probably 'over-specified' in + // that they test behaviors client code shouldn't have to / want to care about. + + @Test + public void noKnownDomains() throws Exception { + assertRoutes(ImmutableList.of(), + "applications:\n" + + "- name: my-app\n" + + " routes:\n" + + " - route: some.thing.com\n" + // => + /* NONE */ + ); + assertRoutes(ImmutableList.of(), + "applications:\n" + + "- name: my-app\n" + // => + /* NONE */ + ); + } + + @Test + public void bogusDomain() throws Exception { + assertRoutes(ImmutableList.of(), + "applications:\n" + + "- name: my-app\n" + + " random-route: true\n" + + " domain: bogus.com\n" + + " domains: [ bogus.me, cfapps.io]\n" + , // => + + //Rationale for the below results. When determining random-route behavior + // for unknown domain assume its http, i.e. the most common case. + "?@bogus.me" , + "?@cfapps.io", + "?@bogus.com" + ); + } + + //////////////////////////////////////////////////////////////////////////////////// + private void assertRoutes(String manifestText, String... expectedRoutes) { + assertRoutes(TEST_DOMAINS, manifestText, expectedRoutes); + } + + private void assertRoutes(List domains, String manifestText, String... expectedRoutes) { + //Why the loop? This improves test coverage. The test should pass the same repeatedly, its behavior + // should not change. It *might* change if we have some kind of bug where cached state influences the + // behavior inadvertently in a manner visible to a caller. + for (int i = 0; i < 3; i++) { + RouteBuilder rb = new RouteBuilder(domains); + List routes = rb.buildRoutes(parse(domains, + manifestText)); + StringBuilder expected = new StringBuilder(); + StringBuilder expectedUri = new StringBuilder(); + for (String r : expectedRoutes) { + expected.append(r+"\n"); + expectedUri.append(r.replace("@", ".")); + } + StringBuilder actual = new StringBuilder(); + StringBuilder actualUri = new StringBuilder(); + for (RouteBinding r : routes) { + actual.append(r+"\n"); + actualUri.append(r.toUri()); + } + assertEquals(expected.toString(), actual.toString()); + assertEquals(expectedUri.toString(), actualUri.toString()); + } + } + + /** + * Parse a manifest with a single + * @param manifestText + * @return + */ + private RouteAttributes parse(List domains, String manifestText) { + IDocument doc = new Document(manifestText); + YamlASTProvider parser = new YamlASTProvider(new Yaml()); + YamlFileAST ast = parser.getAST(doc); + List names = YamlPath.EMPTY + .thenAnyChild() + .thenValAt("applications") + .thenAnyChild() + .thenValAt("name") + .traverseAmbiguously(ast) + .collect(Collectors.toList()); + assertEquals("Number of apps in manifest", 1, names.size()); + String appName = NodeUtil.asScalar(names.get(0)); + CloudData cloudData = new CloudData(domains, "some-buildpack", ImmutableList.of()); + YamlGraphDeploymentProperties dp = new YamlGraphDeploymentProperties(manifestText, appName, cloudData); + return new RouteAttributes(dp); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/SelectDockerDaemonDialogTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/SelectDockerDaemonDialogTest.java new file mode 100644 index 000000000..93896111c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/SelectDockerDaemonDialogTest.java @@ -0,0 +1,38 @@ +package org.springframework.ide.eclipse.boot.dash.test; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.docker.ui.SelectDockerDaemonDialog; +import org.springframework.ide.eclipse.boot.dash.docker.ui.SelectDockerDaemonDialog.Model; + +public class SelectDockerDaemonDialogTest { + + private static final String DEFAULT_DOCKER_URL = "unix:///var/run/docker.sock"; + + @Test + public void initialValues() throws Exception { + Model model = new SelectDockerDaemonDialog.Model(); + assertEquals(false, model.okPressed.getValue()); + assertEquals(true, model.useLocalDaemon.getValue()); + assertEquals(false, model.daemonUrlEnabled.getValue()); + assertEquals(DEFAULT_DOCKER_URL, model.daemonUrl.getValue()); + } + + @Test + public void customUrlWidgets() throws Exception { + Model model = new SelectDockerDaemonDialog.Model(); + assertEquals(true, model.useLocalDaemon.getValue()); + assertEquals(false, model.daemonUrlEnabled.getValue()); + assertEquals(DEFAULT_DOCKER_URL, model.daemonUrl.getValue()); + + model.useLocalDaemon.setValue(false); + assertEquals(true, model.daemonUrlEnabled.getValue()); + model.daemonUrl.setValue("custom"); + assertEquals("custom", model.daemonUrl.getValue()); + model.useLocalDaemon.setValue(true); + assertEquals(false, model.daemonUrlEnabled.getValue()); + assertEquals(DEFAULT_DOCKER_URL, model.daemonUrl.getValue()); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/TestBootDashModelContext.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/TestBootDashModelContext.java new file mode 100644 index 000000000..df07b0a37 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/TestBootDashModelContext.java @@ -0,0 +1,175 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; + +import java.io.File; +import java.util.regex.Pattern; + +import org.apache.commons.io.FileUtils; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; +import org.eclipse.debug.core.ILaunchManager; +import org.springframework.ide.eclipse.boot.core.BootPreferences; +import org.springframework.ide.eclipse.boot.core.cli.BootInstallManager; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.cf.actions.CfBootDashActions; +import org.springframework.ide.eclipse.boot.dash.cf.client.CloudFoundryClientFactory; +import org.springframework.ide.eclipse.boot.dash.cf.debug.DebugSupport; +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshDebugSupport; +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshTunnelFactory; +import org.springframework.ide.eclipse.boot.dash.cf.jmxtunnel.JmxSshTunnelManager; +import org.springframework.ide.eclipse.boot.dash.console.CloudAppLogManager; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +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.SecuredCredentialsStore; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockScopedPropertyStore; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockSecuredCredentialStore; +import org.springframework.ide.eclipse.boot.dash.test.mocks.MockSshTunnel; +import org.springframework.ide.eclipse.boot.dash.views.BootDashActions; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.IScopedPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.InMemoryPropertyStore; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +public class TestBootDashModelContext extends BootDashModelContext { + + private File stateLoc; + private File installLoc; + private ILaunchManager launchManager; + private IWorkspace workspace; + SecuredCredentialsStore secureStore = new MockSecuredCredentialStore(); + private IScopedPropertyStore projectProperties; + private IScopedPropertyStore runTargetProperties; + private LiveVariable bootProjectExclusion = new LiveVariable<>(BootPreferences.DEFAULT_BOOT_PROJECT_EXCLUDE); + private IPropertyStore viewProperties = new InMemoryPropertyStore(); + private IPropertyStore privateProperties = new InMemoryPropertyStore(); + private IPropertyStore installProperties = new InMemoryPropertyStore(); + private BootInstallManager bootInstalls; + + public TestBootDashModelContext(IWorkspace workspace, ILaunchManager launchMamager) { + super(createInjections()); + try { + this.workspace = workspace; + this.launchManager = launchMamager; + stateLoc = StsTestUtil.createTempDirectory("plugin-state", null); + installLoc = StsTestUtil.createTempDirectory("boot-installs", null); + this.projectProperties = new MockScopedPropertyStore<>(); + this.runTargetProperties = new MockScopedPropertyStore<>(); + reload(); + } catch (Exception e) { + throw ExceptionUtil.unchecked(e); + } + } + + private static SimpleDIContext createInjections() { + SimpleDIContext injections = new SimpleDIContext(); + injections.defInstance(AllUserInteractions.class, mock(AllUserInteractions.class)); + injections.defInstance(BootDashActions.Factory.class, CfBootDashActions.factory); + injections.defInstance(DebugSupport.class, SshDebugSupport.INSTANCE); + injections.def(BootDashViewModel.class, BootDashViewModel::new); + injections.defInstance(SshTunnelFactory.class, MockSshTunnel::new); + injections.defInstance(JmxSshTunnelManager.class, new JmxSshTunnelManager()); + injections.def(CloudAppLogManager.class, CloudAppLogManager::new); //TODO: replace with a mock? + return injections; + } + + public IPath getStateLocation() { + return new Path(stateLoc.toString()); + } + + public IWorkspace getWorkspace() { + return workspace; + } + + public void teardownn() throws Exception { + FileUtils.deleteQuietly(stateLoc); + } + + public ILaunchManager getLaunchManager() { + return launchManager; + } + + @Override + public void log(Exception e) { + // No implementation we'll use Mockito to spy on the method instead. + } + + @Override + public IScopedPropertyStore getProjectProperties() { + return projectProperties; + } + + @Override + public IScopedPropertyStore getRunTargetProperties() { + return runTargetProperties; + } + + @Override + public SecuredCredentialsStore getSecuredCredentialsStore() { + return secureStore; + } + + @Override + public LiveExpression getBootProjectExclusion() { + return bootProjectExclusion; + } + + @Override + public IPropertyStore getViewProperties() { + return viewProperties; + } + + @Override + public IPropertyStore getPrivatePropertyStore() { + return privateProperties; + } + + @Override + public BootInstallManager getBootInstallManager() { + return bootInstalls; + } + + /** + * Simulates reloading + * @return + */ + public TestBootDashModelContext reload() throws Exception { + this.bootInstalls = new BootInstallManager(installLoc, installProperties); + injections.reload(); + return this; + } + + public TestBootDashModelContext withTargetTypes(RunTargetType... types) { + assertFalse("Multiple initializations of RunTargetTypes ?", injections.hasDefinitionFor(RunTargetType.class)); + for (RunTargetType t : types) { + injections.defInstance(RunTargetType.class, t); + } + return this; + } + + public TestBootDashModelContext withCfClient(CloudFoundryClientFactory client) { + injections.defInstance(CloudFoundryClientFactory.class, client); + return this; + } + + public RunTargetType getRargetTypeWithName(String name) { + return injections.getBeans(RunTargetType.class).stream().filter(t -> t.getName().equals(name)).findFirst().orElse(null); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/ToggleFiltersModelTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/ToggleFiltersModelTest.java new file mode 100644 index 000000000..9e5f80aa3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/ToggleFiltersModelTest.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test; + +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.hasItemInArray; +import static org.hamcrest.Matchers.hasToString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.eclipse.core.resources.IProject; +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement; +import org.springframework.ide.eclipse.boot.dash.model.LaunchConfDashElement; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel.FilterChoice; +import org.springsource.ide.eclipse.commons.core.pstore.InMemoryPropertyStore; +import org.springsource.ide.eclipse.commons.livexp.util.Filter; + +import com.google.common.collect.ImmutableSet; + +@SuppressWarnings("unchecked") +public class ToggleFiltersModelTest { + + private static final String HIDE_NON_WORKSPACE_ELEMENTS = "Hide non-workspace elements"; + private static final String HIDE_SOLITARY_CONF = "Hide solitary launch configs"; + private static final String HIDE_LOCAL_SERVICES = "Hide local cloud services"; + + private InMemoryPropertyStore propertyStore = new InMemoryPropertyStore(); + + @Test + public void testAvailableFilters() throws Exception { + ToggleFiltersModel model = new ToggleFiltersModel(propertyStore); + assertThat(model.getAvailableFilters(), + arrayContaining( + hasToString("FilterChoice("+HIDE_NON_WORKSPACE_ELEMENTS+")"), + hasToString("FilterChoice("+HIDE_SOLITARY_CONF+")"), + hasToString("FilterChoice("+HIDE_LOCAL_SERVICES+")") + ) + ); + } + + @Test + public void testHideNonWorkspaceElementsToleratesNull() throws Exception { + //Case: element == null + // Basically this shouldn't happen, but if it does, the filter does + // something sensible. + + Filter f = getFilter(HIDE_NON_WORKSPACE_ELEMENTS); + + assertEquals(false, f.accept(null)); + } + + @Test + public void testHideNonWorkspaceElementsNullProject() throws Exception { + //Case: project == null means not associated with ws project + + Filter f = getFilter(HIDE_NON_WORKSPACE_ELEMENTS); + + BootDashElement e = mock(BootDashElement.class); + when(e.getProject()).thenReturn(null); // unnessary, but just for clarity + assertEquals(false, f.accept(e)); + } + + @Test + public void testHideNonWorkspaceElementsProjectNoExist() throws Exception { + //Case 2" project that doesn't exist. + // => may have been associated with a project, but project no longer in workspace + + Filter f = getFilter(HIDE_NON_WORKSPACE_ELEMENTS); + BootDashElement e = mock(BootDashElement.class); + IProject p = mock(IProject.class); + + when(e.getProject()).thenReturn(p); + when(p.exists()).thenReturn(false); + + assertEquals(false, f.accept(e)); + } + + @Test + public void testHideNonWorkspaceElementsProjectExist() throws Exception { + //Case 2" project that doesn't exist. + // => may have been associated with a project, but project no longer in workspace + + Filter f = getFilter(HIDE_NON_WORKSPACE_ELEMENTS); + BootDashElement e = mock(BootDashElement.class); + IProject p = mock(IProject.class); + + when(e.getProject()).thenReturn(p); + when(p.exists()).thenReturn(true); + + assertEquals(true, f.accept(e)); + } + + @Test + public void testHideSolitaryConfEnabledByDefault() throws Exception { + ToggleFiltersModel model = new ToggleFiltersModel(propertyStore); + assertThat(model.getAvailableFilters(), + hasItemInArray( + hasToString("FilterChoice("+HIDE_SOLITARY_CONF+")") + ) + ); + } + + @Test + public void testHideSolitaryConf() throws Exception { + Filter filter = getFilter(HIDE_SOLITARY_CONF); + BootProjectDashElement project = mock(BootProjectDashElement.class); + LaunchConfDashElement conf1 = mock(LaunchConfDashElement.class); + + when(conf1.getParent()).thenReturn(project); + when(project.getCurrentChildren()).thenReturn(ImmutableSet.of(conf1)); + + assertTrue(filter.accept(project)); + assertFalse(filter.accept(conf1)); + } + + @Test + public void testShowNonSolitaryConf() throws Exception { + Filter filter = getFilter(HIDE_SOLITARY_CONF); + BootProjectDashElement project = mock(BootProjectDashElement.class); + LaunchConfDashElement conf1 = mock(LaunchConfDashElement.class); + LaunchConfDashElement conf2 = mock(LaunchConfDashElement.class); + + when(conf1.getParent()).thenReturn(project); + when(conf2.getParent()).thenReturn(project); + + when(project.getCurrentChildren()).thenReturn(ImmutableSet.of(conf1, conf2)); + + assertTrue(filter.accept(project)); + assertTrue(filter.accept(conf1)); + assertTrue(filter.accept(conf2)); + } + + @Test + public void testToggleFiltersPersistAndRestore() throws Exception { + ToggleFiltersModel model = new ToggleFiltersModel(propertyStore); + + FilterChoice nonWorkspace = getFilter(model, HIDE_NON_WORKSPACE_ELEMENTS); + FilterChoice solitaryConf = getFilter(model, HIDE_SOLITARY_CONF); + FilterChoice localService = getFilter(model, HIDE_LOCAL_SERVICES); + + //initially the defaults should be set. + assertEquals(ImmutableSet.of(solitaryConf, localService), model.getSelectedFilters().getValues()); + + model.getSelectedFilters().remove(solitaryConf); + model.getSelectedFilters().add(nonWorkspace); + + //Simulate model reload (i.e. just instantiate it with the same property store). + model = new ToggleFiltersModel(propertyStore); + + assertEquals(ImmutableSet.of(nonWorkspace, localService), model.getSelectedFilters().getValues()); + } + + private Filter getFilter(String withLabel) { + ToggleFiltersModel model = new ToggleFiltersModel(propertyStore); + FilterChoice selectFilter = getFilter(model, withLabel); + model.getSelectedFilters().replaceAll(ImmutableSet.of(selectFilter)); + Filter effectiveFilter = model.getFilter().getValue(); + return effectiveFilter; + } + + private FilterChoice getFilter(ToggleFiltersModel model, String withLabel) { + for (FilterChoice choice : model.getAvailableFilters()) { + if (choice.getLabel().equals(withLabel)) { + return choice; + } + } + fail("No available filter has this label '"+withLabel+"'"); + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/ActuatorDataTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/ActuatorDataTest.java new file mode 100644 index 000000000..5e21060c0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/ActuatorDataTest.java @@ -0,0 +1,339 @@ +/******************************************************************************* + * Copyright (c) 2017, 2020 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.test.actuator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.io.InputStream; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.junit.Test; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBean; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansContext; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansModel; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; +import org.springframework.ide.eclipse.boot.dash.model.actuator.ActuatorClient; +import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.LiveEnvModel; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.Profile; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.Property; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.PropertySource; +import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * Tests data obtained from the Actuator + * + * @author Alex Boyko + * @author Nieraj Singh + */ +public class ActuatorDataTest { + + static class TestActuatorClient extends ActuatorClient { + + private String version = null; + private String beansJson = ""; + private String requestMappingsJson = ""; + private String envJson = ""; + + + public TestActuatorClient(TypeLookup typeLookup) { + super(typeLookup); + } + + public TestActuatorClient version(String version) { + this.version = version; + return this; + } + + public TestActuatorClient envJson(String json) { + this.envJson = json; + return this; + } + + public TestActuatorClient beansJson(String json) { + this.beansJson = json; + return this; + } + + public TestActuatorClient requestMappingJson(String json) { + this.requestMappingsJson = json; + return this; + } + + @Override + protected ImmutablePair getRequestMappingData() throws Exception { + return ImmutablePair.of(requestMappingsJson, version); + } + + @Override + protected ImmutablePair getBeansData() throws Exception { + return ImmutablePair.of(beansJson, version); + } + + @Override + protected ImmutablePair getEnvData() throws Exception { + return ImmutablePair.of(envJson, version); + } + + } + + + public static String getContents(String resourcePath) throws Exception { + InputStream input = ActuatorDataTest.class.getResourceAsStream(resourcePath); + String s = IOUtil.toString(input); + System.out.println(s); + return s; + } + + @Test public void testModelEquality() throws Exception { + TestActuatorClient client = new TestActuatorClient(null).beansJson(getContents("beans-sample.json")).version("1"); + LiveBeansModel liveBeans = client.getBeans(); + assertEquals(liveBeans, client.getBeans()); + } + + @Test public void testModelIneuality_1() throws Exception { + TestActuatorClient client = new TestActuatorClient(null).beansJson(getContents("beans-sample.json")).version("1"); + TestActuatorClient otherClient = new TestActuatorClient(null).beansJson(getContents("beans-sample-diff1.json")).version("1"); + assertNotEquals(client.getBeans(), otherClient.getBeans()); + } + + @Test public void testModelContent() throws Exception { + TestActuatorClient client = new TestActuatorClient(null).beansJson(getContents("beans-sample.json")).version("1"); + LiveBeansModel liveBeans = client.getBeans(); + assertEquals(1, liveBeans.getBeansByContext().size()); + LiveBeansContext context = liveBeans.getBeansByContext().get(0); + assertEquals(2, context.getElements().size()); + assertEquals(2, liveBeans.getBeans().size()); + assertEquals(2, liveBeans.getBeansByResource().size()); + + assertEquals(context.getElements(), liveBeans.getBeans()); + + LiveBean bean1 = liveBeans.getBeans().get(0); + assertEquals( + "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration$StandardJackson2ObjectMapperBuilderCustomizer", + bean1.getBeanType()); + assertEquals("standardJacksonObjectMapperBuilderCustomizer", bean1.getId()); + + LiveBean bean2 = liveBeans.getBeans().get(1); + assertEquals( + "org.springframework.boot.autoconfigure.jackson.JacksonProperties", + bean2.getBeanType()); + assertEquals("spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties", bean2.getId()); + } + + @Test public void testModelContentBoot2() throws Exception { + TestActuatorClient client = new TestActuatorClient(null).beansJson(getContents("beans-sample-boot2-1.json")).version("2"); + LiveBeansModel liveBeans = client.getBeans(); + assertEquals(1, liveBeans.getBeansByContext().size()); + LiveBeansContext context = liveBeans.getBeansByContext().get(0); + assertEquals(2, context.getElements().size()); + assertEquals(2, liveBeans.getBeans().size()); + assertEquals(2, liveBeans.getBeansByResource().size()); + + assertEquals(context.getElements(), liveBeans.getBeans()); + + // Boot 2 order of beans is arbitrary due to JSONObject implementation. + + LiveBean bean1 = liveBeans.getBeans().stream().filter(b -> "standardJacksonObjectMapperBuilderCustomizer".equals(b.getId())).findFirst().orElse(null); + assertNotNull(bean1); + assertEquals( + "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration$StandardJackson2ObjectMapperBuilderCustomizer", + bean1.getBeanType()); + assertEquals("standardJacksonObjectMapperBuilderCustomizer", bean1.getId()); + + LiveBean bean2 = liveBeans.getBeans().stream().filter(b -> "spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties".equals(b.getId())).findFirst().orElse(null); + assertNotNull(bean2); + assertEquals( + "org.springframework.boot.autoconfigure.jackson.JacksonProperties", + bean2.getBeanType()); + assertEquals("spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties", bean2.getId()); + } + + @Test public void testRequestMappingsBoot2() throws Exception { + TestActuatorClient client = new TestActuatorClient(null).version("2").requestMappingJson(getContents("requestmappings-sample-boot2.json")); + List mappings = client.getRequestMappings(); + System.out.println(mappings); + ImmutableSet expected = ImmutableSet.of( + "/**/favicon.ico", + "/actuator/health", + "/actuator/info", + "/actuator", + "/hello", + "/greeting", + "/error", + "/webjars/**", + "/**" + ); + assertEquals(expected, + mappings.stream() + .map(RequestMapping::getPath) + .collect(Collectors.toSet()) + ); + } + + @Test public void testRequestMappingsBoot2WebFlux() throws Exception { + TestActuatorClient client = new TestActuatorClient(null).version("2").requestMappingJson(getContents("requestmappings-boot2-webflux.json")); + List mappings = client.getRequestMappings(); + System.out.println(mappings); + ImmutableSet expected = ImmutableSet.of( + "/actuator", + "/actuator/health", + "/actuator/info", + "/webjars/**", + "/hello", + "/pp", + "/qq", + "/**" + ); + assertEquals(expected, + mappings.stream() + .map(RequestMapping::getPath) + .collect(Collectors.toSet()) + ); + } + + @Test public void testRequestMappingsBoot2WebFluxFunctional() throws Exception { + TestActuatorClient client = new TestActuatorClient(null).version("2").requestMappingJson(getContents("requestmappings-boot2-webflux-functional.json")); + List mappings = client.getRequestMappings(); + System.out.println(mappings); + ImmutableSet expected = ImmutableSet.of( + "/actuator", + "/actuator/health", + "/actuator/info", + "/webjars/**", + "/hello", + "/**" + ); + assertEquals(expected, + mappings.stream() + .map(RequestMapping::getPath) + .collect(Collectors.toSet()) + ); + } + + @Test public void testRequestMappingsWithManualServletRegistration() throws Exception { + TestActuatorClient client = new TestActuatorClient(null).version("2").requestMappingJson(getContents("requestmappings-with-manual-servlet-registration.json")); + List mappings = client.getRequestMappings(); + System.out.println(mappings); + ImmutableSet expected = ImmutableSet.of( + "/actuator/beans", + "/error", + "/webjars/**", + "/**", + "/**", + "/service/*", + "/services/*", + "/hello" + ); + assertEquals(expected, + mappings.stream() + .map(RequestMapping::getPath) + .collect(Collectors.toSet()) + ); + } + + @Test public void testEnvModelEquality() throws Exception { + TestActuatorClient client = new TestActuatorClient(null).envJson(getContents("env-sample-boot2.json")).version("2"); + LiveEnvModel env = client.getEnv(); + assertEquals(env, client.getEnv()); + } + + @Test public void testEnvModelInequality() throws Exception { + TestActuatorClient client = new TestActuatorClient(null) + .envJson(getContents("env-sample-boot2.json")).version("2"); + TestActuatorClient otherClient = new TestActuatorClient(null) + .envJson(getContents("env-sample-boot2-diff.json")).version("2"); + + LiveEnvModel env = client.getEnv(); + LiveEnvModel otherEnv = otherClient.getEnv(); + assertNotEquals(env, otherEnv); + + assertNotEquals(env.getActiveProfiles().getProfiles(), otherEnv.getActiveProfiles().getProfiles()); + assertNotEquals(env.getPropertySources().getPropertySources(), + otherEnv.getPropertySources().getPropertySources()); + } + + @Test public void testEnvModelContent() throws Exception { + TestActuatorClient client = new TestActuatorClient(null) + .envJson(getContents("env-sample-boot2.json")).version("2"); + LiveEnvModel env = client.getEnv(); + assertEquals(ImmutableList.of(new Profile("production"), new Profile("staging")), + env.getActiveProfiles().getProfiles()); + + // Check a property source with no origin + PropertySource noOriginPropertySource = env.getPropertySources().getPropertySources().stream() + .filter(propertysource -> "server.ports".equals(propertysource.getDisplayName())).findFirst() + .orElse(null); + assertNotNull(noOriginPropertySource); + assertEquals(1, noOriginPropertySource.getProperties().size()); + assertEquals("local.server.port", noOriginPropertySource.getProperties().get(0).getName()); + assertEquals("8080", noOriginPropertySource.getProperties().get(0).getValue()); + assertNull(noOriginPropertySource.getProperties().get(0).getOrigin()); + + // Check a property source with origin + PropertySource propertySource = env.getPropertySources().getPropertySources().stream() + .filter(propertysource -> "systemEnvironment".equals(propertysource.getDisplayName())).findFirst() + .orElse(null); + assertNotNull(propertySource); + Property property = propertySource.getProperties().stream() + .filter(prop -> "JAVA_MAIN_CLASS_43370".equals(prop.getName())).findFirst().orElse(null); + assertNotNull(property); + assertEquals("hello.Application", property.getValue()); + assertEquals("System Environment Property \"JAVA_MAIN_CLASS_43370\"", property.getOrigin().getOrigin()); + } + + @Test public void testEnvModelEqualityBoot1x() throws Exception { + TestActuatorClient client = new TestActuatorClient(null).envJson(getContents("env-sample-boot1.json")).version("1"); + LiveEnvModel env = client.getEnv(); + assertEquals(env, client.getEnv()); + } + + @Test public void testEnvModelContentBoot1x() throws Exception { + TestActuatorClient client = new TestActuatorClient(null) + .envJson(getContents("env-sample-boot1.json")).version("1"); + LiveEnvModel env = client.getEnv(); + + PropertySource propertySource = env.getPropertySources().getPropertySources().stream() + .filter(propertysource -> "server.ports".equals(propertysource.getDisplayName())).findFirst() + .orElse(null); + assertNotNull(propertySource); + assertEquals(1, propertySource.getProperties().size()); + assertEquals("local.server.port", propertySource.getProperties().get(0).getName()); + assertEquals("8090", propertySource.getProperties().get(0).getValue()); + + propertySource = env.getPropertySources().getPropertySources().stream() + .filter(propertysource -> "commandLineArgs".equals(propertysource.getDisplayName())).findFirst() + .orElse(null); + assertNotNull(propertySource); + assertEquals(1, propertySource.getProperties().size()); + assertEquals("spring.output.ansi.enabled", propertySource.getProperties().get(0).getName()); + assertEquals("always", propertySource.getProperties().get(0).getValue()); + + propertySource = env.getPropertySources().getPropertySources().stream() + .filter(propertysource -> "systemProperties".equals(propertysource.getDisplayName())).findFirst() + .orElse(null); + assertNotNull(propertySource); + + Property property = propertySource.getProperties().stream() + .filter(prop -> "file.encoding.pkg".equals(prop.getName())).findFirst().orElse(null); + assertNotNull(property); + assertEquals("sun.io", property.getValue()); + + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/RequestMappingAsserts.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/RequestMappingAsserts.java new file mode 100644 index 000000000..58342ce37 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/RequestMappingAsserts.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test.actuator; + +import static org.junit.Assert.fail; + +import java.util.List; + +import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping; + +public class RequestMappingAsserts { + + public static RequestMapping assertRequestMappingWithPath(List mappings, String string) { + StringBuilder builder = new StringBuilder(); + for (RequestMapping m : mappings) { + builder.append(m.getPath()+"\n"); + if (m.getPath().equals(string)) { + return m; + } + } + fail( + "Expected path not found: "+string+"\n" + + "Found:\n" + + builder + ); + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/beans-sample-boot2-1.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/beans-sample-boot2-1.json new file mode 100644 index 000000000..5829e08e9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/beans-sample-boot2-1.json @@ -0,0 +1,23 @@ +{ + "parent": null, + "beans": { + "standardJacksonObjectMapperBuilderCustomizer": { + "aliases": [], + "resource": "class path resource [org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration.class]", + "scope": "singleton", + "type": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration$StandardJackson2ObjectMapperBuilderCustomizer", + "dependencies": [ + "org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4b9e255", + "spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties" + ] + }, + "spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties": { + "aliases": [], + "resource": "null", + "scope": "singleton", + "type": "org.springframework.boot.autoconfigure.jackson.JacksonProperties", + "dependencies": [] + } + }, + "contextId": "application:9999" +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/beans-sample-diff1.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/beans-sample-diff1.json new file mode 100644 index 000000000..cbc63d907 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/beans-sample-diff1.json @@ -0,0 +1,25 @@ +[{ + "parent": null, + "beans": [ + { + "aliases": [], + "resource": "class path resource [org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration1.class]", + "scope": "singleton", + "type": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration$StandardJackson2ObjectMapperBuilderCustomizer", + "bean": "standardJacksonObjectMapperBuilderCustomizer", + "dependencies": [ + "org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4b9e255", + "spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties" + ] + }, + { + "aliases": [], + "resource": "null", + "scope": "singleton", + "type": "org.springframework.boot.autoconfigure.jackson.JacksonProperties", + "bean": "spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties", + "dependencies": [] + } + ], + "context": "application:9999" +}] diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/beans-sample.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/beans-sample.json new file mode 100644 index 000000000..787869197 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/beans-sample.json @@ -0,0 +1,25 @@ +[{ + "parent": null, + "beans": [ + { + "aliases": [], + "resource": "class path resource [org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration.class]", + "scope": "singleton", + "type": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration$StandardJackson2ObjectMapperBuilderCustomizer", + "bean": "standardJacksonObjectMapperBuilderCustomizer", + "dependencies": [ + "org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4b9e255", + "spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties" + ] + }, + { + "aliases": [], + "resource": "null", + "scope": "singleton", + "type": "org.springframework.boot.autoconfigure.jackson.JacksonProperties", + "bean": "spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties", + "dependencies": [] + } + ], + "context": "application:9999" +}] diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/env-sample-boot1.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/env-sample-boot1.json new file mode 100644 index 000000000..8f99d6b18 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/env-sample-boot1.json @@ -0,0 +1,102 @@ +{ + "profiles": [], + "server.ports": { + "local.server.port": 8090 + }, + "commandLineArgs": { + "spring.output.ansi.enabled": "always" + }, + "servletContextInitParams": { + + }, + "systemProperties": { + "com.sun.management.jmxremote.authenticate": "false", + "java.runtime.name": "Java(TM) SE Runtime Environment", + "sun.boot.library.path": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib", + "java.vm.version": "25.191-b12", + "gopherProxySet": "false", + "java.vm.vendor": "Oracle Corporation", + "java.vendor.url": "https://java.oracle.com/", + "java.rmi.server.randomIDs": "true", + "path.separator": ":", + "java.vm.name": "Java HotSpot(TM) 64-Bit Server VM", + "file.encoding.pkg": "sun.io", + "user.country": "CA", + "sun.java.launcher": "SUN_STANDARD", + "sun.os.patch.level": "unknown", + "PID": "44941", + "com.sun.management.jmxremote.port": "51297", + "java.vm.specification.name": "Java Virtual Machine Specification", + "user.dir": "/Users/nsinghpvtl/NS__WORK/sts4dev/rt-boot-ls/rest-boot-1x", + "java.runtime.version": "1.8.0_191-b12", + "java.awt.graphicsenv": "sun.awt.CGraphicsEnvironment", + "org.jboss.logging.provider": "slf4j", + "java.endorsed.dirs": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/endorsed", + "os.arch": "x86_64", + "java.io.tmpdir": "/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/", + "line.separator": "\n", + "java.vm.specification.vendor": "Oracle Corporation", + "os.name": "Mac OS X", + "sun.jnu.encoding": "UTF-8", + "spring.beaninfo.ignore": "true", + "java.library.path": "/Users/nsinghpvtl/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.", + "java.specification.name": "Java Platform API Specification", + "java.class.version": "52.0", + "sun.management.compiler": "HotSpot 64-Bit Tiered Compilers", + "os.version": "10.14", + "http.nonProxyHosts": "local|*.local|169.254/16|*.169.254/16", + "user.home": "/Users/nsinghpvtl", + "catalina.useNaming": "false", + "user.timezone": "America/Los_Angeles", + "java.awt.printerjob": "sun.lwawt.macosx.CPrinterJob", + "file.encoding": "UTF-8", + "java.specification.version": "1.8", + "catalina.home": "/private/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/tomcat.4185031704588200795.8080", + "java.class.path": "/Users/nsinghpvtl/NS__WORK/sts4dev/rt-boot-ls/rest-boot-1x/target/classes:/Users/nsinghpvtl/.m2/repository/org/springframework/boot/spring-boot-starter-actuator/1.5.19.RELEASE/spring-boot-starter-actuator-1.5.19.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/boot/spring-boot-starter/1.5.19.RELEASE/spring-boot-starter-1.5.19.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/boot/spring-boot/1.5.19.RELEASE/spring-boot-1.5.19.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/1.5.19.RELEASE/spring-boot-autoconfigure-1.5.19.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/boot/spring-boot-starter-logging/1.5.19.RELEASE/spring-boot-starter-logging-1.5.19.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/ch/qos/logback/logback-classic/1.1.11/logback-classic-1.1.11.jar:/Users/nsinghpvtl/.m2/repository/ch/qos/logback/logback-core/1.1.11/logback-core-1.1.11.jar:/Users/nsinghpvtl/.m2/repository/org/slf4j/jcl-over-slf4j/1.7.25/jcl-over-slf4j-1.7.25.jar:/Users/nsinghpvtl/.m2/repository/org/slf4j/jul-to-slf4j/1.7.25/jul-to-slf4j-1.7.25.jar:/Users/nsinghpvtl/.m2/repository/org/slf4j/log4j-over-slf4j/1.7.25/log4j-over-slf4j-1.7.25.jar:/Users/nsinghpvtl/.m2/repository/org/yaml/snakeyaml/1.17/snakeyaml-1.17.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/boot/spring-boot-actuator/1.5.19.RELEASE/spring-boot-actuator-1.5.19.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/spring-context/4.3.22.RELEASE/spring-context-4.3.22.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/boot/spring-boot-starter-web/1.5.19.RELEASE/spring-boot-starter-web-1.5.19.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/1.5.19.RELEASE/spring-boot-starter-tomcat-1.5.19.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/8.5.37/tomcat-embed-core-8.5.37.jar:/Users/nsinghpvtl/.m2/repository/org/apache/tomcat/tomcat-annotations-api/8.5.37/tomcat-annotations-api-8.5.37.jar:/Users/nsinghpvtl/.m2/repository/org/apache/tomcat/embed/tomcat-embed-el/8.5.37/tomcat-embed-el-8.5.37.jar:/Users/nsinghpvtl/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/8.5.37/tomcat-embed-websocket-8.5.37.jar:/Users/nsinghpvtl/.m2/repository/org/hibernate/hibernate-validator/5.3.6.Final/hibernate-validator-5.3.6.Final.jar:/Users/nsinghpvtl/.m2/repository/javax/validation/validation-api/1.1.0.Final/validation-api-1.1.0.Final.jar:/Users/nsinghpvtl/.m2/repository/org/jboss/logging/jboss-logging/3.3.2.Final/jboss-logging-3.3.2.Final.jar:/Users/nsinghpvtl/.m2/repository/com/fasterxml/classmate/1.3.4/classmate-1.3.4.jar:/Users/nsinghpvtl/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.8.11.3/jackson-databind-2.8.11.3.jar:/Users/nsinghpvtl/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.8.0/jackson-annotations-2.8.0.jar:/Users/nsinghpvtl/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.8.11/jackson-core-2.8.11.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/spring-web/4.3.22.RELEASE/spring-web-4.3.22.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/spring-aop/4.3.22.RELEASE/spring-aop-4.3.22.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/spring-beans/4.3.22.RELEASE/spring-beans-4.3.22.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/spring-webmvc/4.3.22.RELEASE/spring-webmvc-4.3.22.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/spring-expression/4.3.22.RELEASE/spring-expression-4.3.22.RELEASE.jar:/Users/nsinghpvtl/.m2/repository/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar:/Users/nsinghpvtl/.m2/repository/org/springframework/spring-core/4.3.22.RELEASE/spring-core-4.3.22.RELEASE.jar", + "user.name": "nsinghpvtl", + "com.sun.management.jmxremote": "", + "java.vm.specification.version": "1.8", + "sun.java.command": "******", + "java.home": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre", + "sun.arch.data.model": "64", + "user.language": "en", + "java.specification.vendor": "Oracle Corporation", + "awt.toolkit": "sun.lwawt.macosx.LWCToolkit", + "com.sun.management.jmxremote.ssl": "false", + "java.vm.info": "mixed mode", + "java.version": "1.8.0_191", + "java.ext.dirs": "/Users/nsinghpvtl/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java", + "sun.boot.class.path": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/classes", + "java.awt.headless": "true", + "java.vendor": "Oracle Corporation", + "catalina.base": "/private/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/tomcat.4185031704588200795.8080", + "spring.application.admin.enabled": "true", + "java.security.egd": "file:/dev/./urandom", + "file.separator": "/", + "java.vendor.url.bug": "https://bugreport.sun.com/bugreport/", + "sun.io.unicode.encoding": "UnicodeBig", + "sun.cpu.endian": "little", + "java.rmi.server.hostname": "localhost", + "socksNonProxyHosts": "local|*.local|169.254/16|*.169.254/16", + "ftp.nonProxyHosts": "local|*.local|169.254/16|*.169.254/16", + "sun.cpu.isalist": "" + }, + "systemEnvironment": { + "APP_ICON_36880": "../Resources/sts4.icns", + "PATH": "/usr/bin:/bin:/usr/sbin:/sbin", + "SHELL": "/bin/bash", + "JAVA_MAIN_CLASS_44921": "org.eclipse.equinox.launcher.Main", + "SQLITE_EXEMPT_PATH_FROM_VNODE_GUARDS": "/Users/nsinghpvtl/Library/WebKit/Databases", + "JAVA_MAIN_CLASS_44941": "com.example.demo.RestBoot1xApplication", + "USER": "nsinghpvtl", + "TMPDIR": "/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/", + "SSH_AUTH_SOCK": "/private/tmp/com.apple.launchd.u3ySv0SI6p/Listeners", + "XPC_FLAGS": "0x0", + "__CF_USER_TEXT_ENCODING": "0x1F5:0x0:0x52", + "Apple_PubSub_Socket_Render": "/private/tmp/com.apple.launchd.Dh6f52yVMk/Render", + "LOGNAME": "nsinghpvtl", + "XPC_SERVICE_NAME": "org.springframework.boot.ide.branding.sts4.5948", + "HOME": "/Users/nsinghpvtl", + "JAVA_STARTED_ON_FIRST_THREAD_44921": "1" + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/env-sample-boot2-diff.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/env-sample-boot2-diff.json new file mode 100644 index 000000000..864a532ca --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/env-sample-boot2-diff.json @@ -0,0 +1,311 @@ +{ + "activeProfiles": [ + "production", + "differentstaging" + ], + "propertySources": [ + { + "name": "server.ports", + "properties": { + "local.server.port": { + "value": 8085 + } + } + }, + { + "name": "commandLineArgs", + "properties": { + "spring.output.ansi.enabled": { + "value": "always" + }, + "spring.profiles.active": { + "value": "production, differentstaging" + } + } + }, + { + "name": "servletContextInitParams", + "properties": { + + } + }, + { + "name": "systemProperties", + "properties": { + "com.sun.management.jmxremote.authenticate": { + "value": "false" + }, + "java.runtime.name": { + "value": "Java(TM) SE Runtime Environment" + }, + "sun.boot.library.path": { + "value": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib" + }, + "java.vm.version": { + "value": "25.191-b12" + }, + "gopherProxySet": { + "value": "false" + }, + "java.vm.vendor": { + "value": "Oracle Corporation" + }, + "java.vendor.url": { + "value": "https://java.oracle.com/" + }, + "java.rmi.server.randomIDs": { + "value": "true" + }, + "path.separator": { + "value": ":" + }, + "java.vm.name": { + "value": "Java HotSpot(TM) 64-Bit Server VM" + }, + "file.encoding.pkg": { + "value": "sun.io" + }, + "user.country": { + "value": "CA" + }, + "sun.java.launcher": { + "value": "SUN_STANDARD" + }, + "PID": { + "value": "4337033" + }, + "com.sun.management.jmxremote.port": { + "value": "49208" + }, + "java.vm.specification.name": { + "value": "Java Virtual Machine Specification" + }, + "user.dir": { + "value": "/Users/user/workingfolder/sts4dev/rt-boot-ls/gs-rest-service-complete" + }, + "java.runtime.version": { + "value": "1.8.0_191-b12" + }, + "java.awt.graphicsenv": { + "value": "sun.awt.CGraphicsEnvironment" + }, + "java.endorsed.dirs": { + "value": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/endorsed" + }, + "os.arch": { + "value": "x86_64" + }, + "java.io.tmpdir": { + "value": "/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/" + }, + "line.separator": { + "value": "\n" + }, + "java.vm.specification.vendor": { + "value": "Oracle Corporation" + }, + "os.name": { + "value": "Mac OS X" + }, + "sun.jnu.encoding": { + "value": "UTF-8" + }, + "spring.beaninfo.ignore": { + "value": "true" + }, + "java.library.path": { + "value": "/Users/user/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:." + }, + "java.specification.name": { + "value": "Java Platform API Specification" + }, + "java.class.version": { + "value": "52.0" + }, + "sun.management.compiler": { + "value": "HotSpot 64-Bit Tiered Compilers" + }, + "os.version": { + "value": "10.14" + }, + "http.nonProxyHosts": { + "value": "local|*.local|169.254/16|*.169.254/16" + }, + "user.home": { + "value": "/Users/user" + }, + "catalina.useNaming": { + "value": "false" + }, + "user.timezone": { + "value": "America/Los_Angeles" + }, + "java.awt.printerjob": { + "value": "sun.lwawt.macosx.CPrinterJob" + }, + "file.encoding": { + "value": "UTF-8" + }, + "java.specification.version": { + "value": "1.8" + }, + "catalina.home": { + "value": "/private/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/tomcat.3339174210667415129.8080" + }, + "java.class.path": { + "value": "/Users/user/workingfolder/sts4dev/rt-boot-ls/gs-rest-service-complete/target/classes:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-web/2.0.5.RELEASE/52daa1f1509bd637a737206e54c06a17aabb9504/spring-boot-starter-web-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-json/2.0.5.RELEASE/d0052ded4733ceb1fb7d927238f22f9a92099227/spring-boot-starter-json-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter/2.0.5.RELEASE/1f53487a373be18d064a5815e9bac9882ef15cdc/spring-boot-starter-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-tomcat/2.0.5.RELEASE/eaac8a5d73b45400bc88cd7f6b5c99b5f0d5e9b7/spring-boot-starter-tomcat-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.hibernate.validator/hibernate-validator/6.0.12.Final/478003e61b056c1f97840ba3e62ff31cdc89597/hibernate-validator-6.0.12.Final.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-webmvc/5.0.9.RELEASE/c18346caaeb8dc648c4cc01874996fd9fef76664/spring-webmvc-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-web/5.0.9.RELEASE/1ea3aab93340849313fa74ec626ddaf1fff9ed8e/spring-web-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-autoconfigure/2.0.5.RELEASE/e5588642799e0c0c04638e255c3d3f31ba400ff4/spring-boot-autoconfigure-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot/2.0.5.RELEASE/19a4624cbd89a318d10c79f289c6c816043850fb/spring-boot-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-logging/2.0.5.RELEASE/c353e0b9591d0765c687ff0a678478cbebfd5c23/spring-boot-starter-logging-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/javax.annotation/javax.annotation-api/1.3.2/934c04d3cfef185a8008e7bf34331b79730a9d43/javax.annotation-api-1.3.2.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-context/5.0.9.RELEASE/2501e55acb6c2e84667cda3f845d1d00a0dc4e05/spring-context-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-aop/5.0.9.RELEASE/98003b099697fe46b6bdf18c7e3f66d7a1381060/spring-aop-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-beans/5.0.9.RELEASE/65f56fdab1bb90ad059e314d2f2f4cf76f9bdbde/spring-beans-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-expression/5.0.9.RELEASE/1f9db5ff3a758102c0434cc3457aa47c50c39a4a/spring-expression-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-core/5.0.9.RELEASE/9f9a828936d81afd49a603bda9cc1aed863a0d85/spring-core-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.yaml/snakeyaml/1.19/2d998d3d674b172a588e54ab619854d073f555b5/snakeyaml-1.19.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.9.6/456895fc91bf7180b216fead220373e6278230c9/jackson-datatype-jdk8-2.9.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.9.6/ea54f6193d224e5e5732bbd4262327eb465397c2/jackson-datatype-jsr310-2.9.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.module/jackson-module-parameter-names/2.9.6/129acd77a4b6ee30d62d3a0899b1344c8ec2bff8/jackson-module-parameter-names-2.9.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-databind/2.9.6/cfa4f316351a91bfd95cb0644c6a2c95f52db1fc/jackson-databind-2.9.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-websocket/8.5.34/5f86906367c2540b21e6aeecc277d2ce9ec939b0/tomcat-embed-websocket-8.5.34.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-core/8.5.34/a038040d68a90397f95dd1e11b979fe364a5000f/tomcat-embed-core-8.5.34.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-el/8.5.34/be71a9a5bdd001db7cf97c47429eec0bdd3b7b88/tomcat-embed-el-8.5.34.jar:/Users/user/.gradle/caches/modules-2/files-2.1/javax.validation/validation-api/2.0.1.Final/cb855558e6271b1b32e716d24cb85c7f583ce09e/validation-api-2.0.1.Final.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.jboss.logging/jboss-logging/3.3.2.Final/3789d00e859632e6c6206adc0c71625559e6e3b0/jboss-logging-3.3.2.Final.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml/classmate/1.3.4/3d5f48f10bbe4eb7bd862f10c0583be2e0053c6/classmate-1.3.4.jar:/Users/user/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.2.3/7c4f3c474fb2c041d8028740440937705ebb473a/logback-classic-1.2.3.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-to-slf4j/2.10.0/f7e631ccf49cfc0aefa4a2a728da7d374c05bd3c/log4j-to-slf4j-2.10.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.slf4j/jul-to-slf4j/1.7.25/af5364cd6679bfffb114f0dec8a157aaa283b76/jul-to-slf4j-1.7.25.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-jcl/5.0.9.RELEASE/bc3b5aaae53f0bc03647e53ecbd98a05b47a4e90/spring-jcl-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-annotations/2.9.0/7c10d545325e3a6e72e06381afe469fd40eb701/jackson-annotations-2.9.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.9.6/4e393793c37c77e042ccc7be5a914ae39251b365/jackson-core-2.9.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.2.3/864344400c3d4d92dfeb0a305dc87d953677c03c/logback-core-1.2.3.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.25/da76ca59f6a57ee3102f8f9bd9cee742973efa8a/slf4j-api-1.7.25.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.10.0/fec5797a55b786184a537abd39c3fa1449d752d6/log4j-api-2.10.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-test/2.0.5.RELEASE/ab04ca2e76f6a8e786001352a81a10e4b0e7fbc8/spring-boot-starter-test-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.jayway.jsonpath/json-path/2.4.0/765a4401ceb2dc8d40553c2075eb80a8fa35c2ae/json-path-2.4.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-test-autoconfigure/2.0.5.RELEASE/54d5e8f9e88c6236fe164474f0aed5a8a1052c43/spring-boot-test-autoconfigure-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-test/2.0.5.RELEASE/ce6353f33217982357a6bdba6576015ea333304c/spring-boot-test-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/junit/junit/4.12/2973d150c0dc1fefe998f834810d68f278ea58ec/junit-4.12.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.assertj/assertj-core/3.9.1/c5ce126b15f28d56cd8f960c1a6a058b9c9aea87/assertj-core-3.9.1.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.mockito/mockito-core/2.15.0/b84bfbbc29cd22c9529409627af6ea2897f4fa85/mockito-core-2.15.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.hamcrest/hamcrest-library/1.3/4785a3c21320980282f9f33d0d1264a69040538f/hamcrest-library-1.3.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.hamcrest/hamcrest-core/1.3/42a25dc3219429f0e5d060061f71acb49bf010a0/hamcrest-core-1.3.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.skyscreamer/jsonassert/1.5.0/6c9d5fe2f59da598d9aefc1cfc6528ff3cf32df3/jsonassert-1.5.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-test/5.0.9.RELEASE/218c8648f898453be92d550252e0ce2a84b54375/spring-test-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.xmlunit/xmlunit-core/2.5.1/4ffdb346572a7356f7521cd3119ce5287d2e339d/xmlunit-core-2.5.1.jar:/Users/user/.gradle/caches/modules-2/files-2.1/net.minidev/json-smart/2.3/7396407491352ce4fa30de92efb158adb76b5b/json-smart-2.3.jar:/Users/user/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy/1.7.11/f02857a4e2c66ccbe7aaad6100a0a6c461bce9b3/byte-buddy-1.7.11.jar:/Users/user/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy-agent/1.7.11/b425a8933ca07edd03c6dbc8bc3b595fba9780de/byte-buddy-agent-1.7.11.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.objenesis/objenesis/2.6/639033469776fd37c08358c6b92a4761feb2af4b/objenesis-2.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.vaadin.external.google/android-json/0.0.20131108.vaadin1/fa26d351fe62a6a17f5cda1287c1c6110dec413f/android-json-0.0.20131108.vaadin1.jar:/Users/user/.gradle/caches/modules-2/files-2.1/net.minidev/accessors-smart/1.2/c592b500269bfde36096641b01238a8350f8aa31/accessors-smart-1.2.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm/5.0.4/da08b8cce7bbf903602a25a3a163ae252435795/asm-5.0.4.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter-web/2.0.5.RELEASE/spring-boot-starter-web-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter/2.0.5.RELEASE/spring-boot-starter-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot/2.0.5.RELEASE/spring-boot-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.0.5.RELEASE/spring-boot-autoconfigure-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter-logging/2.0.5.RELEASE/spring-boot-starter-logging-2.0.5.RELEASE.jar:/Users/user/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar:/Users/user/.m2/repository/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar:/Users/user/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.10.0/log4j-to-slf4j-2.10.0.jar:/Users/user/.m2/repository/org/apache/logging/log4j/log4j-api/2.10.0/log4j-api-2.10.0.jar:/Users/user/.m2/repository/org/slf4j/jul-to-slf4j/1.7.25/jul-to-slf4j-1.7.25.jar:/Users/user/.m2/repository/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.jar:/Users/user/.m2/repository/org/yaml/snakeyaml/1.19/snakeyaml-1.19.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter-json/2.0.5.RELEASE/spring-boot-starter-json-2.0.5.RELEASE.jar:/Users/user/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.9.6/jackson-databind-2.9.6.jar:/Users/user/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.9.0/jackson-annotations-2.9.0.jar:/Users/user/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.9.6/jackson-core-2.9.6.jar:/Users/user/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.9.6/jackson-datatype-jdk8-2.9.6.jar:/Users/user/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.9.6/jackson-datatype-jsr310-2.9.6.jar:/Users/user/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.9.6/jackson-module-parameter-names-2.9.6.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/2.0.5.RELEASE/spring-boot-starter-tomcat-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/8.5.34/tomcat-embed-core-8.5.34.jar:/Users/user/.m2/repository/org/apache/tomcat/embed/tomcat-embed-el/8.5.34/tomcat-embed-el-8.5.34.jar:/Users/user/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/8.5.34/tomcat-embed-websocket-8.5.34.jar:/Users/user/.m2/repository/org/hibernate/validator/hibernate-validator/6.0.12.Final/hibernate-validator-6.0.12.Final.jar:/Users/user/.m2/repository/javax/validation/validation-api/2.0.1.Final/validation-api-2.0.1.Final.jar:/Users/user/.m2/repository/org/jboss/logging/jboss-logging/3.3.2.Final/jboss-logging-3.3.2.Final.jar:/Users/user/.m2/repository/com/fasterxml/classmate/1.3.4/classmate-1.3.4.jar:/Users/user/.m2/repository/org/springframework/spring-web/5.0.9.RELEASE/spring-web-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-beans/5.0.9.RELEASE/spring-beans-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-webmvc/5.0.9.RELEASE/spring-webmvc-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-aop/5.0.9.RELEASE/spring-aop-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-context/5.0.9.RELEASE/spring-context-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-expression/5.0.9.RELEASE/spring-expression-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-core/5.0.9.RELEASE/spring-core-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-jcl/5.0.9.RELEASE/spring-jcl-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter-actuator/2.0.5.RELEASE/spring-boot-starter-actuator-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-actuator-autoconfigure/2.0.5.RELEASE/spring-boot-actuator-autoconfigure-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-actuator/2.0.5.RELEASE/spring-boot-actuator-2.0.5.RELEASE.jar:/Users/user/.m2/repository/io/micrometer/micrometer-core/1.0.6/micrometer-core-1.0.6.jar:/Users/user/.m2/repository/org/hdrhistogram/HdrHistogram/2.1.10/HdrHistogram-2.1.10.jar:/Users/user/.m2/repository/org/latencyutils/LatencyUtils/2.0.3/LatencyUtils-2.0.3.jar" + }, + "user.name": { + "value": "user" + }, + "com.sun.management.jmxremote": { + "value": "" + }, + "java.vm.specification.version": { + "value": "1.8" + }, + "sun.java.command": { + "value": "******" + }, + "java.home": { + "value": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre" + }, + "sun.arch.data.model": { + "value": "64" + }, + "user.language": { + "value": "en" + }, + "java.specification.vendor": { + "value": "Oracle Corporation" + }, + "awt.toolkit": { + "value": "sun.lwawt.macosx.LWCToolkit" + }, + "com.sun.management.jmxremote.ssl": { + "value": "false" + }, + "java.vm.info": { + "value": "mixed mode" + }, + "java.version": { + "value": "1.8.0_191" + }, + "java.ext.dirs": { + "value": "/Users/user/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java" + }, + "sun.boot.class.path": { + "value": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/classes" + }, + "java.awt.headless": { + "value": "true" + }, + "java.vendor": { + "value": "Oracle Corporation" + }, + "catalina.base": { + "value": "/private/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/tomcat.3339174210667415129.8080" + }, + "spring.application.admin.enabled": { + "value": "true" + }, + "java.security.egd": { + "value": "file:/dev/./urandom" + }, + "file.separator": { + "value": "/" + }, + "java.vendor.url.bug": { + "value": "https://bugreport.sun.com/bugreport/" + }, + "sun.io.unicode.encoding": { + "value": "UnicodeBig" + }, + "sun.cpu.endian": { + "value": "little" + }, + "java.rmi.server.hostname": { + "value": "localhost" + }, + "socksNonProxyHosts": { + "value": "local|*.local|169.254/16|*.169.254/16" + }, + "ftp.nonProxyHosts": { + "value": "local|*.local|169.254/16|*.169.254/16" + }, + "sun.cpu.isalist": { + "value": "" + } + } + }, + { + "name": "systemEnvironment", + "properties": { + "APP_ICON_36880": { + "value": "../Resources/sts4.icns", + "origin": "System Environment Property \"APP_ICON_36880\"" + }, + "PATH": { + "value": "/usr/bin:/bin:/usr/sbin:/sbin", + "origin": "System Environment Property \"PATH\"" + }, + "SHELL": { + "value": "/bin/bash", + "origin": "System Environment Property \"SHELL\"" + }, + "SQLITE_EXEMPT_PATH_FROM_VNODE_GUARDS": { + "value": "/Users/user/Library/WebKit/Databases", + "origin": "System Environment Property \"SQLITE_EXEMPT_PATH_FROM_VNODE_GUARDS\"" + }, + "USER": { + "value": "user", + "origin": "System Environment Property \"USER\"" + }, + "JAVA_MAIN_CLASS_43363": { + "value": "org.eclipse.equinox.launcher.Main", + "origin": "System Environment Property \"JAVA_MAIN_CLASS_43363\"" + }, + "TMPDIR": { + "value": "/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/", + "origin": "System Environment Property \"TMPDIR\"" + }, + "JAVA_MAIN_CLASS_43370": { + "value": "hello.Application", + "origin": "System Environment Property \"JAVA_MAIN_CLASS_43370\"" + }, + "SSH_AUTH_SOCK": { + "value": "/private/tmp/com.apple.launchd.u3ySv0SI6p/Listeners", + "origin": "System Environment Property \"SSH_AUTH_SOCK\"" + }, + "XPC_FLAGS": { + "value": "0x0", + "origin": "System Environment Property \"XPC_FLAGS\"" + }, + "JAVA_STARTED_ON_FIRST_THREAD_36880": { + "value": "1", + "origin": "System Environment Property \"JAVA_STARTED_ON_FIRST_THREAD_36880\"" + }, + "JAVA_STARTED_ON_FIRST_THREAD_43363": { + "value": "1", + "origin": "System Environment Property \"JAVA_STARTED_ON_FIRST_THREAD_43363\"" + }, + "__CF_USER_TEXT_ENCODING": { + "value": "0x1F5:0x0:0x52", + "origin": "System Environment Property \"__CF_USER_TEXT_ENCODING\"" + }, + "LOGNAME": { + "value": "user", + "origin": "System Environment Property \"LOGNAME\"" + }, + "XPC_SERVICE_NAME": { + "value": "org.springframework.boot.ide.branding.sts4.5948", + "origin": "System Environment Property \"XPC_SERVICE_NAME\"" + }, + "HOME": { + "value": "/Users/user", + "origin": "System Environment Property \"HOME\"" + } + } + } + ] +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/env-sample-boot2.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/env-sample-boot2.json new file mode 100644 index 000000000..1c8363b93 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/env-sample-boot2.json @@ -0,0 +1,318 @@ +{ + "activeProfiles": [ + "production", + "staging" + ], + "propertySources": [ + { + "name": "server.ports", + "properties": { + "local.server.port": { + "value": 8080 + } + } + }, + { + "name": "commandLineArgs", + "properties": { + "spring.output.ansi.enabled": { + "value": "always" + }, + "spring.profiles.active": { + "value": "production, staging" + } + } + }, + { + "name": "servletContextInitParams", + "properties": { + + } + }, + { + "name": "systemProperties", + "properties": { + "com.sun.management.jmxremote.authenticate": { + "value": "false" + }, + "java.runtime.name": { + "value": "Java(TM) SE Runtime Environment" + }, + "sun.boot.library.path": { + "value": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib" + }, + "java.vm.version": { + "value": "25.191-b12" + }, + "gopherProxySet": { + "value": "false" + }, + "java.vm.vendor": { + "value": "Oracle Corporation" + }, + "java.vendor.url": { + "value": "https://java.oracle.com/" + }, + "java.rmi.server.randomIDs": { + "value": "true" + }, + "path.separator": { + "value": ":" + }, + "java.vm.name": { + "value": "Java HotSpot(TM) 64-Bit Server VM" + }, + "file.encoding.pkg": { + "value": "sun.io" + }, + "user.country": { + "value": "CA" + }, + "sun.java.launcher": { + "value": "SUN_STANDARD" + }, + "sun.os.patch.level": { + "value": "unknown" + }, + "PID": { + "value": "43370" + }, + "com.sun.management.jmxremote.port": { + "value": "49208" + }, + "java.vm.specification.name": { + "value": "Java Virtual Machine Specification" + }, + "user.dir": { + "value": "/Users/user/workingfolder/sts4dev/rt-boot-ls/gs-rest-service-complete" + }, + "java.runtime.version": { + "value": "1.8.0_191-b12" + }, + "java.awt.graphicsenv": { + "value": "sun.awt.CGraphicsEnvironment" + }, + "java.endorsed.dirs": { + "value": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/endorsed" + }, + "os.arch": { + "value": "x86_64" + }, + "java.io.tmpdir": { + "value": "/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/" + }, + "line.separator": { + "value": "\n" + }, + "java.vm.specification.vendor": { + "value": "Oracle Corporation" + }, + "os.name": { + "value": "Mac OS X" + }, + "sun.jnu.encoding": { + "value": "UTF-8" + }, + "spring.beaninfo.ignore": { + "value": "true" + }, + "java.library.path": { + "value": "/Users/user/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:." + }, + "java.specification.name": { + "value": "Java Platform API Specification" + }, + "java.class.version": { + "value": "52.0" + }, + "sun.management.compiler": { + "value": "HotSpot 64-Bit Tiered Compilers" + }, + "os.version": { + "value": "10.14" + }, + "http.nonProxyHosts": { + "value": "local|*.local|169.254/16|*.169.254/16" + }, + "user.home": { + "value": "/Users/user" + }, + "catalina.useNaming": { + "value": "false" + }, + "user.timezone": { + "value": "America/Los_Angeles" + }, + "java.awt.printerjob": { + "value": "sun.lwawt.macosx.CPrinterJob" + }, + "file.encoding": { + "value": "UTF-8" + }, + "java.specification.version": { + "value": "1.8" + }, + "catalina.home": { + "value": "/private/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/tomcat.3339174210667415129.8080" + }, + "java.class.path": { + "value": "/Users/user/workingfolder/sts4dev/rt-boot-ls/gs-rest-service-complete/target/classes:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-web/2.0.5.RELEASE/52daa1f1509bd637a737206e54c06a17aabb9504/spring-boot-starter-web-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-json/2.0.5.RELEASE/d0052ded4733ceb1fb7d927238f22f9a92099227/spring-boot-starter-json-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter/2.0.5.RELEASE/1f53487a373be18d064a5815e9bac9882ef15cdc/spring-boot-starter-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-tomcat/2.0.5.RELEASE/eaac8a5d73b45400bc88cd7f6b5c99b5f0d5e9b7/spring-boot-starter-tomcat-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.hibernate.validator/hibernate-validator/6.0.12.Final/478003e61b056c1f97840ba3e62ff31cdc89597/hibernate-validator-6.0.12.Final.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-webmvc/5.0.9.RELEASE/c18346caaeb8dc648c4cc01874996fd9fef76664/spring-webmvc-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-web/5.0.9.RELEASE/1ea3aab93340849313fa74ec626ddaf1fff9ed8e/spring-web-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-autoconfigure/2.0.5.RELEASE/e5588642799e0c0c04638e255c3d3f31ba400ff4/spring-boot-autoconfigure-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot/2.0.5.RELEASE/19a4624cbd89a318d10c79f289c6c816043850fb/spring-boot-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-logging/2.0.5.RELEASE/c353e0b9591d0765c687ff0a678478cbebfd5c23/spring-boot-starter-logging-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/javax.annotation/javax.annotation-api/1.3.2/934c04d3cfef185a8008e7bf34331b79730a9d43/javax.annotation-api-1.3.2.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-context/5.0.9.RELEASE/2501e55acb6c2e84667cda3f845d1d00a0dc4e05/spring-context-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-aop/5.0.9.RELEASE/98003b099697fe46b6bdf18c7e3f66d7a1381060/spring-aop-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-beans/5.0.9.RELEASE/65f56fdab1bb90ad059e314d2f2f4cf76f9bdbde/spring-beans-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-expression/5.0.9.RELEASE/1f9db5ff3a758102c0434cc3457aa47c50c39a4a/spring-expression-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-core/5.0.9.RELEASE/9f9a828936d81afd49a603bda9cc1aed863a0d85/spring-core-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.yaml/snakeyaml/1.19/2d998d3d674b172a588e54ab619854d073f555b5/snakeyaml-1.19.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.9.6/456895fc91bf7180b216fead220373e6278230c9/jackson-datatype-jdk8-2.9.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.9.6/ea54f6193d224e5e5732bbd4262327eb465397c2/jackson-datatype-jsr310-2.9.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.module/jackson-module-parameter-names/2.9.6/129acd77a4b6ee30d62d3a0899b1344c8ec2bff8/jackson-module-parameter-names-2.9.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-databind/2.9.6/cfa4f316351a91bfd95cb0644c6a2c95f52db1fc/jackson-databind-2.9.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-websocket/8.5.34/5f86906367c2540b21e6aeecc277d2ce9ec939b0/tomcat-embed-websocket-8.5.34.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-core/8.5.34/a038040d68a90397f95dd1e11b979fe364a5000f/tomcat-embed-core-8.5.34.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-el/8.5.34/be71a9a5bdd001db7cf97c47429eec0bdd3b7b88/tomcat-embed-el-8.5.34.jar:/Users/user/.gradle/caches/modules-2/files-2.1/javax.validation/validation-api/2.0.1.Final/cb855558e6271b1b32e716d24cb85c7f583ce09e/validation-api-2.0.1.Final.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.jboss.logging/jboss-logging/3.3.2.Final/3789d00e859632e6c6206adc0c71625559e6e3b0/jboss-logging-3.3.2.Final.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml/classmate/1.3.4/3d5f48f10bbe4eb7bd862f10c0583be2e0053c6/classmate-1.3.4.jar:/Users/user/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.2.3/7c4f3c474fb2c041d8028740440937705ebb473a/logback-classic-1.2.3.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-to-slf4j/2.10.0/f7e631ccf49cfc0aefa4a2a728da7d374c05bd3c/log4j-to-slf4j-2.10.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.slf4j/jul-to-slf4j/1.7.25/af5364cd6679bfffb114f0dec8a157aaa283b76/jul-to-slf4j-1.7.25.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-jcl/5.0.9.RELEASE/bc3b5aaae53f0bc03647e53ecbd98a05b47a4e90/spring-jcl-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-annotations/2.9.0/7c10d545325e3a6e72e06381afe469fd40eb701/jackson-annotations-2.9.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.9.6/4e393793c37c77e042ccc7be5a914ae39251b365/jackson-core-2.9.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.2.3/864344400c3d4d92dfeb0a305dc87d953677c03c/logback-core-1.2.3.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.25/da76ca59f6a57ee3102f8f9bd9cee742973efa8a/slf4j-api-1.7.25.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.10.0/fec5797a55b786184a537abd39c3fa1449d752d6/log4j-api-2.10.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-test/2.0.5.RELEASE/ab04ca2e76f6a8e786001352a81a10e4b0e7fbc8/spring-boot-starter-test-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.jayway.jsonpath/json-path/2.4.0/765a4401ceb2dc8d40553c2075eb80a8fa35c2ae/json-path-2.4.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-test-autoconfigure/2.0.5.RELEASE/54d5e8f9e88c6236fe164474f0aed5a8a1052c43/spring-boot-test-autoconfigure-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-test/2.0.5.RELEASE/ce6353f33217982357a6bdba6576015ea333304c/spring-boot-test-2.0.5.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/junit/junit/4.12/2973d150c0dc1fefe998f834810d68f278ea58ec/junit-4.12.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.assertj/assertj-core/3.9.1/c5ce126b15f28d56cd8f960c1a6a058b9c9aea87/assertj-core-3.9.1.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.mockito/mockito-core/2.15.0/b84bfbbc29cd22c9529409627af6ea2897f4fa85/mockito-core-2.15.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.hamcrest/hamcrest-library/1.3/4785a3c21320980282f9f33d0d1264a69040538f/hamcrest-library-1.3.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.hamcrest/hamcrest-core/1.3/42a25dc3219429f0e5d060061f71acb49bf010a0/hamcrest-core-1.3.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.skyscreamer/jsonassert/1.5.0/6c9d5fe2f59da598d9aefc1cfc6528ff3cf32df3/jsonassert-1.5.0.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.springframework/spring-test/5.0.9.RELEASE/218c8648f898453be92d550252e0ce2a84b54375/spring-test-5.0.9.RELEASE.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.xmlunit/xmlunit-core/2.5.1/4ffdb346572a7356f7521cd3119ce5287d2e339d/xmlunit-core-2.5.1.jar:/Users/user/.gradle/caches/modules-2/files-2.1/net.minidev/json-smart/2.3/7396407491352ce4fa30de92efb158adb76b5b/json-smart-2.3.jar:/Users/user/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy/1.7.11/f02857a4e2c66ccbe7aaad6100a0a6c461bce9b3/byte-buddy-1.7.11.jar:/Users/user/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy-agent/1.7.11/b425a8933ca07edd03c6dbc8bc3b595fba9780de/byte-buddy-agent-1.7.11.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.objenesis/objenesis/2.6/639033469776fd37c08358c6b92a4761feb2af4b/objenesis-2.6.jar:/Users/user/.gradle/caches/modules-2/files-2.1/com.vaadin.external.google/android-json/0.0.20131108.vaadin1/fa26d351fe62a6a17f5cda1287c1c6110dec413f/android-json-0.0.20131108.vaadin1.jar:/Users/user/.gradle/caches/modules-2/files-2.1/net.minidev/accessors-smart/1.2/c592b500269bfde36096641b01238a8350f8aa31/accessors-smart-1.2.jar:/Users/user/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm/5.0.4/da08b8cce7bbf903602a25a3a163ae252435795/asm-5.0.4.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter-web/2.0.5.RELEASE/spring-boot-starter-web-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter/2.0.5.RELEASE/spring-boot-starter-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot/2.0.5.RELEASE/spring-boot-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.0.5.RELEASE/spring-boot-autoconfigure-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter-logging/2.0.5.RELEASE/spring-boot-starter-logging-2.0.5.RELEASE.jar:/Users/user/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar:/Users/user/.m2/repository/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar:/Users/user/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.10.0/log4j-to-slf4j-2.10.0.jar:/Users/user/.m2/repository/org/apache/logging/log4j/log4j-api/2.10.0/log4j-api-2.10.0.jar:/Users/user/.m2/repository/org/slf4j/jul-to-slf4j/1.7.25/jul-to-slf4j-1.7.25.jar:/Users/user/.m2/repository/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.jar:/Users/user/.m2/repository/org/yaml/snakeyaml/1.19/snakeyaml-1.19.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter-json/2.0.5.RELEASE/spring-boot-starter-json-2.0.5.RELEASE.jar:/Users/user/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.9.6/jackson-databind-2.9.6.jar:/Users/user/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.9.0/jackson-annotations-2.9.0.jar:/Users/user/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.9.6/jackson-core-2.9.6.jar:/Users/user/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.9.6/jackson-datatype-jdk8-2.9.6.jar:/Users/user/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.9.6/jackson-datatype-jsr310-2.9.6.jar:/Users/user/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.9.6/jackson-module-parameter-names-2.9.6.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/2.0.5.RELEASE/spring-boot-starter-tomcat-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/8.5.34/tomcat-embed-core-8.5.34.jar:/Users/user/.m2/repository/org/apache/tomcat/embed/tomcat-embed-el/8.5.34/tomcat-embed-el-8.5.34.jar:/Users/user/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/8.5.34/tomcat-embed-websocket-8.5.34.jar:/Users/user/.m2/repository/org/hibernate/validator/hibernate-validator/6.0.12.Final/hibernate-validator-6.0.12.Final.jar:/Users/user/.m2/repository/javax/validation/validation-api/2.0.1.Final/validation-api-2.0.1.Final.jar:/Users/user/.m2/repository/org/jboss/logging/jboss-logging/3.3.2.Final/jboss-logging-3.3.2.Final.jar:/Users/user/.m2/repository/com/fasterxml/classmate/1.3.4/classmate-1.3.4.jar:/Users/user/.m2/repository/org/springframework/spring-web/5.0.9.RELEASE/spring-web-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-beans/5.0.9.RELEASE/spring-beans-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-webmvc/5.0.9.RELEASE/spring-webmvc-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-aop/5.0.9.RELEASE/spring-aop-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-context/5.0.9.RELEASE/spring-context-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-expression/5.0.9.RELEASE/spring-expression-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-core/5.0.9.RELEASE/spring-core-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/springframework/spring-jcl/5.0.9.RELEASE/spring-jcl-5.0.9.RELEASE.jar:/Users/user/.m2/repository/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-starter-actuator/2.0.5.RELEASE/spring-boot-starter-actuator-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-actuator-autoconfigure/2.0.5.RELEASE/spring-boot-actuator-autoconfigure-2.0.5.RELEASE.jar:/Users/user/.m2/repository/org/springframework/boot/spring-boot-actuator/2.0.5.RELEASE/spring-boot-actuator-2.0.5.RELEASE.jar:/Users/user/.m2/repository/io/micrometer/micrometer-core/1.0.6/micrometer-core-1.0.6.jar:/Users/user/.m2/repository/org/hdrhistogram/HdrHistogram/2.1.10/HdrHistogram-2.1.10.jar:/Users/user/.m2/repository/org/latencyutils/LatencyUtils/2.0.3/LatencyUtils-2.0.3.jar" + }, + "user.name": { + "value": "user" + }, + "com.sun.management.jmxremote": { + "value": "" + }, + "java.vm.specification.version": { + "value": "1.8" + }, + "sun.java.command": { + "value": "******" + }, + "java.home": { + "value": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre" + }, + "sun.arch.data.model": { + "value": "64" + }, + "user.language": { + "value": "en" + }, + "java.specification.vendor": { + "value": "Oracle Corporation" + }, + "awt.toolkit": { + "value": "sun.lwawt.macosx.LWCToolkit" + }, + "com.sun.management.jmxremote.ssl": { + "value": "false" + }, + "java.vm.info": { + "value": "mixed mode" + }, + "java.version": { + "value": "1.8.0_191" + }, + "java.ext.dirs": { + "value": "/Users/user/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java" + }, + "sun.boot.class.path": { + "value": "/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/classes" + }, + "java.awt.headless": { + "value": "true" + }, + "java.vendor": { + "value": "Oracle Corporation" + }, + "catalina.base": { + "value": "/private/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/tomcat.3339174210667415129.8080" + }, + "spring.application.admin.enabled": { + "value": "true" + }, + "java.security.egd": { + "value": "file:/dev/./urandom" + }, + "file.separator": { + "value": "/" + }, + "java.vendor.url.bug": { + "value": "https://bugreport.sun.com/bugreport/" + }, + "sun.io.unicode.encoding": { + "value": "UnicodeBig" + }, + "sun.cpu.endian": { + "value": "little" + }, + "java.rmi.server.hostname": { + "value": "localhost" + }, + "socksNonProxyHosts": { + "value": "local|*.local|169.254/16|*.169.254/16" + }, + "ftp.nonProxyHosts": { + "value": "local|*.local|169.254/16|*.169.254/16" + }, + "sun.cpu.isalist": { + "value": "" + } + } + }, + { + "name": "systemEnvironment", + "properties": { + "APP_ICON_36880": { + "value": "../Resources/sts4.icns", + "origin": "System Environment Property \"APP_ICON_36880\"" + }, + "PATH": { + "value": "/usr/bin:/bin:/usr/sbin:/sbin", + "origin": "System Environment Property \"PATH\"" + }, + "SHELL": { + "value": "/bin/bash", + "origin": "System Environment Property \"SHELL\"" + }, + "SQLITE_EXEMPT_PATH_FROM_VNODE_GUARDS": { + "value": "/Users/user/Library/WebKit/Databases", + "origin": "System Environment Property \"SQLITE_EXEMPT_PATH_FROM_VNODE_GUARDS\"" + }, + "USER": { + "value": "user", + "origin": "System Environment Property \"USER\"" + }, + "JAVA_MAIN_CLASS_43363": { + "value": "org.eclipse.equinox.launcher.Main", + "origin": "System Environment Property \"JAVA_MAIN_CLASS_43363\"" + }, + "TMPDIR": { + "value": "/var/folders/0l/sdmkvnyd7vxcm3825k_ynj8w0000gn/T/", + "origin": "System Environment Property \"TMPDIR\"" + }, + "JAVA_MAIN_CLASS_43370": { + "value": "hello.Application", + "origin": "System Environment Property \"JAVA_MAIN_CLASS_43370\"" + }, + "SSH_AUTH_SOCK": { + "value": "/private/tmp/com.apple.launchd.u3ySv0SI6p/Listeners", + "origin": "System Environment Property \"SSH_AUTH_SOCK\"" + }, + "XPC_FLAGS": { + "value": "0x0", + "origin": "System Environment Property \"XPC_FLAGS\"" + }, + "JAVA_STARTED_ON_FIRST_THREAD_36880": { + "value": "1", + "origin": "System Environment Property \"JAVA_STARTED_ON_FIRST_THREAD_36880\"" + }, + "JAVA_STARTED_ON_FIRST_THREAD_43363": { + "value": "1", + "origin": "System Environment Property \"JAVA_STARTED_ON_FIRST_THREAD_43363\"" + }, + "__CF_USER_TEXT_ENCODING": { + "value": "0x1F5:0x0:0x52", + "origin": "System Environment Property \"__CF_USER_TEXT_ENCODING\"" + }, + "Apple_PubSub_Socket_Render": { + "value": "/private/tmp/com.apple.launchd.Dh6f52yVMk/Render", + "origin": "System Environment Property \"Apple_PubSub_Socket_Render\"" + }, + "LOGNAME": { + "value": "user", + "origin": "System Environment Property \"LOGNAME\"" + }, + "XPC_SERVICE_NAME": { + "value": "org.springframework.boot.ide.branding.sts4.5948", + "origin": "System Environment Property \"XPC_SERVICE_NAME\"" + }, + "HOME": { + "value": "/Users/user", + "origin": "System Environment Property \"HOME\"" + } + } + } + ] +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-boot2-webflux-functional.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-boot2-webflux-functional.json new file mode 100644 index 000000000..6d889f58e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-boot2-webflux-functional.json @@ -0,0 +1,133 @@ +{ + "contexts": { + "application": { + "mappings": { + "dispatcherHandlers": { + "webHandler": [ + { + "predicate": "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "handler": "public org.reactivestreams.Publisher> org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping$ReadOperationHandler.handle(org.springframework.web.server.ServerWebExchange)", + "details": { + "handlerFunction": null, + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator/health" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "handle", + "className": "org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping.ReadOperationHandler", + "descriptor": "(Lorg/springframework/web/server/ServerWebExchange;)Lorg/reactivestreams/Publisher;" + } + } + }, + { + "predicate": "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "handler": "public org.reactivestreams.Publisher> org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping$ReadOperationHandler.handle(org.springframework.web.server.ServerWebExchange)", + "details": { + "handlerFunction": null, + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator/info" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "handle", + "className": "org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping.ReadOperationHandler", + "descriptor": "(Lorg/springframework/web/server/ServerWebExchange;)Lorg/reactivestreams/Publisher;" + } + } + }, + { + "predicate": "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "handler": "protected java.util.Map> org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping.links(org.springframework.web.server.ServerWebExchange)", + "details": { + "handlerFunction": null, + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "links", + "className": "org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping", + "descriptor": "(Lorg/springframework/web/server/ServerWebExchange;)Ljava/util/Map;" + } + } + }, + { + "predicate": "((GET && /hello) && Accept: [text/plain])", + "handler": "hello.GreetingRouter$$Lambda$235/720770771@5ebd56e9", + "details": { + "handlerFunction": { + "className": "hello.GreetingRouter$$Lambda$235/720770771" + }, + "requestMappingConditions": null, + "handlerMethod": null + } + }, + { + "predicate": "/webjars/**", + "handler": "ResourceWebHandler [locations=[class path resource [META-INF/resources/webjars/]], resolvers=[org.springframework.web.reactive.resource.PathResourceResolver@347ce31]]", + "details": null + }, + { + "predicate": "/**", + "handler": "ResourceWebHandler [locations=[class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/]], resolvers=[org.springframework.web.reactive.resource.PathResourceResolver@2bdd186e]]", + "details": null + } + ] + } + }, + "parentId": null + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-boot2-webflux.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-boot2-webflux.json new file mode 100644 index 000000000..983b92157 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-boot2-webflux.json @@ -0,0 +1,163 @@ +{ + "contexts": { + "application": { + "mappings": { + "dispatcherHandlers": { + "webHandler": [ + { + "predicate": "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "handler": "public org.reactivestreams.Publisher> org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping$ReadOperationHandler.handle(org.springframework.web.server.ServerWebExchange)", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator/health" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "handle", + "className": "org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping.ReadOperationHandler", + "descriptor": "(Lorg/springframework/web/server/ServerWebExchange;)Lorg/reactivestreams/Publisher;" + } + } + }, + { + "predicate": "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "handler": "public org.reactivestreams.Publisher> org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping$ReadOperationHandler.handle(org.springframework.web.server.ServerWebExchange)", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator/info" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "handle", + "className": "org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping.ReadOperationHandler", + "descriptor": "(Lorg/springframework/web/server/ServerWebExchange;)Lorg/reactivestreams/Publisher;" + } + } + }, + { + "predicate": "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "handler": "protected java.util.Map> org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping.links(org.springframework.web.server.ServerWebExchange)", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "links", + "className": "org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping", + "descriptor": "(Lorg/springframework/web/server/ServerWebExchange;)Ljava/util/Map;" + } + } + }, + { + "predicate": "{[/hello],methods=[GET]}", + "handler": "public java.lang.String com.example.demo.MyController.getMethodName(java.lang.String)", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/hello" + ], + "produces": [], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "getMethodName", + "className": "com.example.demo.MyController", + "descriptor": "(Ljava/lang/String;)Ljava/lang/String;" + } + } + }, + { + "predicate": "{[/pp || /qq],methods=[GET]}", + "handler": "public java.lang.String com.example.demo.MyController.hello()", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/pp", + "/qq" + ], + "produces": [], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "hello", + "className": "com.example.demo.MyController", + "descriptor": "()Ljava/lang/String;" + } + } + }, + { + "predicate": "/webjars/**", + "handler": "ResourceWebHandler [locations=[class path resource [META-INF/resources/webjars/]], resolvers=[org.springframework.web.reactive.resource.PathResourceResolver@5f115dc8]]" + }, + { + "predicate": "/**", + "handler": "ResourceWebHandler [locations=[class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/]], resolvers=[org.springframework.web.reactive.resource.PathResourceResolver@526469af]]" + } + ] + } + } + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-sample-boot2.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-sample-boot2.json new file mode 100644 index 000000000..85790840b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-sample-boot2.json @@ -0,0 +1,285 @@ +{ + "contexts": { + "application": { + "mappings": { + "dispatcherServlets": { + "dispatcherServlet": [ + { + "handler": "ResourceHttpRequestHandler [locations=[class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/], class path resource []], resolvers=[org.springframework.web.servlet.resource.PathResourceResolver@474cf171]]", + "predicate": "/**/favicon.ico", + "details": null + }, + { + "handler": "public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map)", + "predicate": "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator/health" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "handle", + "className": "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;" + } + } + }, + { + "handler": "public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map)", + "predicate": "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator/info" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "handle", + "className": "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;" + } + } + }, + { + "handler": "protected java.util.Map> org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping.links(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)", + "predicate": "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "links", + "className": "org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)Ljava/util/Map;" + } + } + }, + { + "handler": "public java.lang.String com.example.demo.MyController.getMethodName(java.lang.String)", + "predicate": "{[/greeting]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [], + "patterns": [ + "/greeting" + ], + "produces": [], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "getMethodName", + "className": "com.example.demo.MyController", + "descriptor": "(Ljava/lang/String;)Ljava/lang/String;" + } + } + }, + { + "handler": "public java.lang.String com.example.demo.MyController.hello()", + "predicate": "{[/hello]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [], + "patterns": [ + "/hello" + ], + "produces": [], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "hello", + "className": "com.example.demo.MyController", + "descriptor": "()Ljava/lang/String;" + } + } + }, + { + "handler": "public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)", + "predicate": "{[/error]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [], + "patterns": [ + "/error" + ], + "produces": [], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "error", + "className": "org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;)Lorg/springframework/http/ResponseEntity;" + } + } + }, + { + "handler": "public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)", + "predicate": "{[/error],produces=[text/html]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [], + "patterns": [ + "/error" + ], + "produces": [ + { + "negated": false, + "mediaType": "text/html" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "errorHtml", + "className": "org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)Lorg/springframework/web/servlet/ModelAndView;" + } + } + }, + { + "handler": "ResourceHttpRequestHandler [locations=[class path resource [META-INF/resources/webjars/]], resolvers=[org.springframework.web.servlet.resource.PathResourceResolver@73713bc2]]", + "predicate": "/webjars/**", + "details": null + }, + { + "handler": "ResourceHttpRequestHandler [locations=[class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/]], resolvers=[org.springframework.web.servlet.resource.PathResourceResolver@7ddca5b1]]", + "predicate": "/**", + "details": null + } + ] + }, + "servletFilters": [ + { + "name": "requestContextFilter", + "className": "org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter", + "servletNameMappings": [], + "urlPatternMappings": [ + "/*" + ] + }, + { + "name": "webMvcMetricsFilter", + "className": "org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter", + "servletNameMappings": [], + "urlPatternMappings": [ + "/*" + ] + }, + { + "name": "Tomcat WebSocket (JSR356) Filter", + "className": "org.apache.tomcat.websocket.server.WsFilter", + "servletNameMappings": [], + "urlPatternMappings": [ + "/*" + ] + }, + { + "name": "httpPutFormContentFilter", + "className": "org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter", + "servletNameMappings": [], + "urlPatternMappings": [ + "/*" + ] + }, + { + "name": "hiddenHttpMethodFilter", + "className": "org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter", + "servletNameMappings": [], + "urlPatternMappings": [ + "/*" + ] + }, + { + "name": "characterEncodingFilter", + "className": "org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter", + "servletNameMappings": [], + "urlPatternMappings": [ + "/*" + ] + }, + { + "name": "httpTraceFilter", + "className": "org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter", + "servletNameMappings": [], + "urlPatternMappings": [ + "/*" + ] + } + ], + "servlets": [ + { + "mappings": [], + "name": "default", + "className": "org.apache.catalina.servlets.DefaultServlet" + }, + { + "mappings": [ + "/" + ], + "name": "dispatcherServlet", + "className": "org.springframework.web.servlet.DispatcherServlet" + } + ] + }, + "parentId": null + } + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-with-manual-servlet-registration.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-with-manual-servlet-registration.json new file mode 100644 index 000000000..52d3b13b5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/requestmappings-with-manual-servlet-registration.json @@ -0,0 +1,183 @@ +{ + "contexts": { + "application": { + "mappings": { + "dispatcherServlets": { + "dispatcherServlet": [ + { + "handler": "Actuator web endpoint 'beans'", + "predicate": "{GET /actuator/beans, produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}", + "details": { + "handlerMethod": { + "className": "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler", + "name": "handle", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;" + }, + "requestMappingConditions": { + "consumes": [], + "headers": [], + "methods": [ + "GET" + ], + "params": [], + "patterns": [ + "/actuator/beans" + ], + "produces": [ + { + "mediaType": "application/vnd.spring-boot.actuator.v3+json", + "negated": false + }, + { + "mediaType": "application/vnd.spring-boot.actuator.v2+json", + "negated": false + }, + { + "mediaType": "application/json", + "negated": false + } + ] + } + } + }, + { + "handler": "org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)", + "predicate": "{ /error}", + "details": { + "handlerMethod": { + "className": "org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController", + "name": "error", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;)Lorg/springframework/http/ResponseEntity;" + }, + "requestMappingConditions": { + "consumes": [], + "headers": [], + "methods": [], + "params": [], + "patterns": [ + "/error" + ], + "produces": [] + } + } + }, + { + "handler": "org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)", + "predicate": "{ /error, produces [text/html]}", + "details": { + "handlerMethod": { + "className": "org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController", + "name": "errorHtml", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)Lorg/springframework/web/servlet/ModelAndView;" + }, + "requestMappingConditions": { + "consumes": [], + "headers": [], + "methods": [], + "params": [], + "patterns": [ + "/error" + ], + "produces": [ + { + "mediaType": "text/html", + "negated": false + } + ] + } + } + }, + { + "handler": "ResourceHttpRequestHandler [\"classpath:/META-INF/resources/webjars/\"]", + "predicate": "/webjars/**", + "details": null + }, + { + "handler": "ResourceHttpRequestHandler [\"classpath:/META-INF/resources/\", \"classpath:/resources/\", \"classpath:/static/\", \"classpath:/public/\", \"/\"]", + "predicate": "/**", + "details": null + } + ] + }, + "servletFilters": [ + { + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [], + "name": "webMvcMetricsFilter", + "className": "org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter" + }, + { + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [], + "name": "requestContextFilter", + "className": "org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter" + }, + { + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [], + "name": "Tomcat WebSocket (JSR356) Filter", + "className": "org.apache.tomcat.websocket.server.WsFilter" + }, + { + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [], + "name": "characterEncodingFilter", + "className": "org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter" + }, + { + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [], + "name": "formContentFilter", + "className": "org.springframework.boot.web.servlet.filter.OrderedFormContentFilter" + } + ], + "servlets": [ + { + "mappings": [], + "name": "default", + "className": "org.apache.catalina.servlets.DefaultServlet" + }, + { + "mappings": [ + "/service/*" + ], + "name": "CXFServlet", + "className": "org.apache.cxf.transport.servlet.CXFServlet" + }, + { + "mappings": [ + "/" + ], + "name": "dispatcherServlet", + "className": "org.springframework.web.servlet.DispatcherServlet" + }, + { + "mappings": [ + "/services/*" + ], + "name": "messageDispatcherServlet", + "className": "org.springframework.ws.transport.http.MessageDispatcherServlet" + }, + { + "mappings": [ + "/hello" + ], + "name": "httpServlet", + "className": "demo.config.ServletConfig$1" + } + ] + }, + "parentId": null + } + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/sample.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/sample.json new file mode 100644 index 000000000..04870b224 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/sample.json @@ -0,0 +1,67 @@ +{ + "/webjars/**":{ + "bean":"resourceHandlerMapping" + }, + "/**":{ + "bean":"resourceHandlerMapping" + }, + "/**/favicon.ico":{ + "bean":"faviconHandlerMapping" + }, + "{[/error],produces=[text/html]}":{ + "bean":"requestMappingHandlerMapping", + "method":"public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)" + }, + "{[/error]}":{ + "bean":"requestMappingHandlerMapping", + "method":"public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)" + }, + "{[/metrics/{name:.*}],methods=[GET]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint.value(java.lang.String)" + }, + "{[/metrics],methods=[GET]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/configprops],methods=[GET]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/health]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(java.security.Principal)" + }, + "{[/info],methods=[GET]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/beans],methods=[GET]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/mappings],methods=[GET]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/autoconfig],methods=[GET]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/env/{name:.*}],methods=[GET]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint.value(java.lang.String)" + }, + "{[/env],methods=[GET]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/trace],methods=[GET]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/dump],methods=[GET]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/sample2.json b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/sample2.json new file mode 100644 index 000000000..20c27cc19 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/actuator/sample2.json @@ -0,0 +1,75 @@ +{ + "/webjars/**":{ + "bean":"resourceHandlerMapping" + }, + "/**":{ + "bean":"resourceHandlerMapping" + }, + "/**/favicon.ico":{ + "bean":"faviconHandlerMapping" + }, + "{[/hello]}":{ + "bean":"requestMappingHandlerMapping", + "method":"public java.lang.String com.example.HelloController.hello()" + }, + "{[/error],produces=[text/html]}":{ + "bean":"requestMappingHandlerMapping", + "method":"public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)" + }, + "{[/error]}":{ + "bean":"requestMappingHandlerMapping", + "method":"public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)" + }, + "{[/autoconfig || /autoconfig.json],methods=[GET],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/configprops || /configprops.json],methods=[GET],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/mappings || /mappings.json],methods=[GET],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/info || /info.json],methods=[GET],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/dump || /dump.json],methods=[GET],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/trace || /trace.json],methods=[GET],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/health || /health.json],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(java.security.Principal)" + }, + "{[/heapdump || /heapdump.json],methods=[GET],produces=[application/octet-stream]}":{ + "bean":"endpointHandlerMapping", + "method":"public void org.springframework.boot.actuate.endpoint.mvc.HeapdumpMvcEndpoint.invoke(boolean,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse) throws java.io.IOException,javax.servlet.ServletException" + }, + "{[/metrics/{name:.*}],methods=[GET],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint.value(java.lang.String)" + }, + "{[/metrics || /metrics.json],methods=[GET],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/beans || /beans.json],methods=[GET],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + }, + "{[/env/{name:.*}],methods=[GET],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint.value(java.lang.String)" + }, + "{[/env || /env.json],methods=[GET],produces=[application/json]}":{ + "bean":"endpointHandlerMapping", + "method":"public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()" + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/CFOrganizationData.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/CFOrganizationData.java new file mode 100644 index 000000000..4e0872342 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/CFOrganizationData.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test.mocks; + +import java.util.UUID; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFOrganization; + +/** + * Data object implementing {@link CFOrganization} + * + * @author Kris De Volder + */ +public class CFOrganizationData implements CFOrganization { + + private String name; + private UUID guid; + + public CFOrganizationData(String name, UUID guid) { + super(); + this.name = name; + this.guid = guid; + } + + @Override + public String getName() { + return name; + } + + @Override + public UUID getGuid() { + return guid; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/CFServiceInstanceData.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/CFServiceInstanceData.java new file mode 100644 index 000000000..a11d2e06f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/CFServiceInstanceData.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test.mocks; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFServiceInstance; + +public class CFServiceInstanceData implements CFServiceInstance { + + private String name; + + public CFServiceInstanceData(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getPlan() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getDashboardUrl() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getService() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getDescription() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getDocumentationUrl() { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/CFSpaceData.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/CFSpaceData.java new file mode 100644 index 000000000..75f8d3cc7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/CFSpaceData.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test.mocks; + +import java.util.UUID; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFOrganization; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFSpace; + +/** + * @author Kris De Volder + */ +public class CFSpaceData implements CFSpace { + + private String name; + private UUID guid; + private CFOrganization org; + + public CFSpaceData(String name, UUID guid, CFOrganization org) { + super(); + this.name = name; + this.guid = guid; + this.org = org; + } + + @Override + public String getName() { + return name; + } + + @Override + public CFOrganization getOrganization() { + return org; + } + + @Override + public UUID getGuid() { + return guid; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockBootDashModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockBootDashModel.java new file mode 100644 index 000000000..aa9f4ef05 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockBootDashModel.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test.mocks; + +import static org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode.SYNC; + +import org.springframework.ide.eclipse.boot.dash.model.AbstractBootDashModel; +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.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.views.BootDashModelConsoleManager; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +/** + * We use mostly use mockito to mock BootDashModel, but some test cases need + * a more fleshed out mock (e.g. to test listeners attached to the model). + * + * @author Kris De Volder + */ +public class MockBootDashModel extends AbstractBootDashModel { + + public MockBootDashModel(RunTarget target, BootDashModelContext context, BootDashViewModel parent) { + super(target, parent); + } + + private LiveSetVariable elements = new LiveSetVariable<>(SYNC); + + @Override + public ObservableSet getElements() { + return elements; + } + + @Override + public BootDashModelConsoleManager getElementConsoleManager() { + return null; + } + + @Override + public void dispose() { + elements = null; + } + + @Override + public void refresh(UserInteractions ui) { + } + + public void add(BootDashElement element) { + elements.add(element); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFApplication.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFApplication.java new file mode 100644 index 000000000..018b34214 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFApplication.java @@ -0,0 +1,434 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test.mocks; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.eclipse.core.runtime.Assert; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFAppState; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplicationDetail; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFInstanceState; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFInstanceStats; +import org.springframework.ide.eclipse.boot.dash.cf.client.InstanceState; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.ApplicationExtras; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFApplicationDetailData; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFApplicationSummaryData; +import org.springframework.ide.eclipse.boot.dash.cf.routes.RouteBinding; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; + +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import com.google.common.collect.ImmutableMap; + +import reactor.core.publisher.Mono; + +public class MockCFApplication { + + private static class MockCFInstanceStats implements CFInstanceStats { + + private CFInstanceState state; + + public MockCFInstanceStats(CFInstanceState state) { + this.state = state; + } + + @Override + public CFInstanceState getState() { + return state; + } + + @Override + public String toString() { + return "CFInstanceState("+state+")"; + } + } + + private final String name; + private final UUID guid; + private int instances; + + private Map env = new HashMap<>(); + private int memory = 1024; + private List services = new ArrayList<>(); + private String buildpackUrl = null; + private List routes = new ArrayList<>(); + private CFAppState state = CFAppState.STOPPED; + private int diskQuota = 1024; + private Integer timeout = null; + private String healthCheckType = DeploymentProperties.DEFAULT_HEALTH_CHECK_TYPE; + private String healthCheckHttpEndpoint = null; + private String command = null; + private String stack = null; + private MockCloudFoundryClientFactory owner; + private MockCFSpace space; + + public MockCFApplication(MockCloudFoundryClientFactory owner, MockCFSpace space, String name, UUID guid, int instances, CFAppState state) { + this.owner = owner; + this.space = space; + this.name = name; + this.guid = guid; + this.instances = instances; + this.state = state; + this.cancelationTokens = new CancelationTokens(); + } + + private ImmutableList stats = ImmutableList.of(); + + private CancelationTokens cancelationTokens; + private Supplier bits; + + public MockCFApplication(MockCloudFoundryClientFactory owner, MockCFSpace space, String name) { + this(owner, + space, + name, + UUID.randomUUID(), + 1, + CFAppState.STOPPED + ); + } + + public String getName() { + return name; + } + + public List getStats() { + return stats; + } + + public void start(CancelationToken cancelationToken) throws Exception { + Assert.isLegal(CFAppState.STOPPED==state); + Assert.isLegal(stats.isEmpty()); + this.state = CFAppState.UNKNOWN; + final long endTime = System.currentTimeMillis()+getStartDelay(); + new ACondition("simulated app starting (waiting)", getStartDelay()+1000) { + @Override + public boolean test() throws Exception { +// System.out.println("Checking token: "+cancelToken); + if (!cancelationToken.isCanceled() && System.currentTimeMillis() builder = ImmutableList.builder(); + for (int i = 0; i < instances; i++) { + Map values = new HashMap<>(); + values.put("state", InstanceState.RUNNING.toString()); + CFInstanceStats stat = new MockCFInstanceStats(CFInstanceState.RUNNING); + builder.add(stat); + } + if (cancelationToken.isCanceled()) { + System.out.println("Starting "+getName()+" CANCELED"); + throw new IOException("Operation Canceled"); + } + this.stats = builder.build(); + this.state = CFAppState.STARTED; + System.out.println("Starting "+getName()+" SUCCESS"); + } + + private long getStartDelay() { + return owner.getStartDelay(); + } + + public String getHealthCheckType() { + return healthCheckType; + } + + public void setHealthCheckType(String t) { + this.healthCheckType = t; + } + + public String getHealthCheckHttpEndpoint() { + return healthCheckHttpEndpoint; + } + + public void setHealthCheckHttpEndpoint(String t) { + this.healthCheckHttpEndpoint = t; + } + + public void setHealthCheckTypeMaybe(String t) { + if (t!=null) { + setHealthCheckType(t); + } + } + + +// public int getInstances() { +// return instances; +// } + + public int getRunningInstances() { + int runningInstances = 0; + for (CFInstanceStats instance : getStats()) { + if (instance.getState()==CFInstanceState.RUNNING) { + runningInstances++; + } + } + return runningInstances; + } + + public UUID getGuid() { + return guid; + } + + public void setBuildpackUrl(String buildpackUrl) { + this.buildpackUrl = buildpackUrl; + } + + public void setCommand(String command) { + this.command = command; + } + + public void setStack(String stack) { + this.stack = stack; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + + public void setDiskQuota(int diskQuota) { + this.diskQuota = diskQuota; + } + + public void setMemory(int memory) { + this.memory = memory; + } + + public int getMemory() { + return this.memory; + } + + public void setRoutes(Collection routes) { + this.routes = routes==null?null:ImmutableList.copyOf(routes); + } + + public void setServices(Collection services) { + this.services = services==null?null:ImmutableList.copyOf(services); + } + + public CFApplication getBasicInfo() { + return new CFApplicationSummaryData( + name, + instances, + getRunningInstances(), + memory, + guid, + getUris(), + state, + diskQuota, + getExtras() + ); + } + + private List getUris() { + return routes.stream() + .map(RouteBinding::toUri) + .collect(Collectors.toList()); + } + + private ApplicationExtras getExtras() { + return new ApplicationExtras() { + @Override + public Mono getStack() { + return Mono.justOrEmpty(stack); + } + @Override + public Mono> getServices() { + return Mono.justOrEmpty(services); + } + + @Override + public Mono> getEnv() { + return Mono.justOrEmpty(env); + } + + @Override + public Mono getBuildpack() { + return Mono.justOrEmpty(buildpackUrl); + } + @Override + public Mono getTimeout() { + return Mono.justOrEmpty(timeout); + } + @Override + public Mono getCommand() { + return Mono.justOrEmpty(command); + } + @Override + public Mono getHealthCheckType() { + return Mono.justOrEmpty(healthCheckType); + } + @Override + public Mono getHealthCheckHttpEndpoint() { + return Mono.justOrEmpty(healthCheckHttpEndpoint); + } + + }; + } + + public CFApplicationDetail getDetailedInfo() { + return new CFApplicationDetailData( + new CFApplicationSummaryData( + name, + instances, + getRunningInstances(), + memory, + guid, + getUris(), + state, + diskQuota, + getExtras() + ), + ImmutableList.copyOf(stats) + ); +// return new CFApplicationDetailData(getBasicInfo(), ImmutableList.copyOf(stats)); + } + + @Override + public String toString() { + return "MockCFApp("+name+")"; + } + + public void stop() { + cancelationTokens.cancelAll(); + this.stats = ImmutableList.of(); + this.state = CFAppState.STOPPED; + } + + public Map getEnv() { + return env; + } + + public void setEnv(Map newEnv) { + env = ImmutableMap.copyOf(newEnv); + } + + public void restart(CancelationToken cancelationToken) throws Exception { + stop(); + start(cancelationToken); + } + + public void scaleInstances(int desiredInstances) { + Assert.isLegal(desiredInstances>0); + Builder builder = ImmutableList.builder(); + builder.addAll(stats); + for (int i = 0; i < desiredInstances; i++) { + builder.add(new MockCFInstanceStats(CFInstanceState.RUNNING)); + } + stats = builder.build(); + this.instances = desiredInstances; + } + + + public void setBuildpackUrlMaybe(String buildpack) { + if (buildpack!=null) { + setBuildpackUrl(buildpack); + } + } + + public void setCommandMaybe(String command) { + if (command!=null) { + setCommand(command); + } + } + + public void setDiskQuotaMaybe(Integer diskQuota) { + if (diskQuota!=null) { + setDiskQuota(diskQuota); + } + } + + public void setEnvMaybe(Map env) { + if (env!=null) { + setEnv(env); + } + } + + public void setMemoryMaybe(Integer memory) { + if (memory!=null) { + setMemory(memory); + } + } + + public void setServicesMaybe(List services) { + if (services!=null) { + setServices(services); + } + } + + public void setStackMaybe(String stack) { + if (stack!=null) { + setStack(stack); + } + } + + public void setTimeoutMaybe(Integer timeout) { + if (timeout!=null) { + setTimeout(timeout); + } + } + + public int getPushCount() { + return space.getPushCount(name).getValue(); + } + + public String getFileContents(String path) throws IOException { + ZipInputStream zip = new ZipInputStream(getBits()); + ZipEntry entry; + while (null != (entry = zip.getNextEntry())) { + if (!entry.isDirectory()) { + if (entry.getName().equals(path)) { + return new String(readBytes(zip)); + } + } + } + return null; + } + + private byte[] readBytes(ZipInputStream zip) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int nextByte; + while ((nextByte = zip.read())>=0) { + bytes.write(nextByte); + } + return bytes.toByteArray(); + } + + public void setBits(Supplier bytes) { + this.bits = bytes; + } + + public ByteArrayInputStream getBits() { + return new ByteArrayInputStream(bits.get()); + } + + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFBuildpack.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFBuildpack.java new file mode 100644 index 000000000..7b827e7f5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFBuildpack.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test.mocks; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFBuildpack; + +public class MockCFBuildpack implements CFBuildpack{ + + private String name; + + public MockCFBuildpack(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFSpace.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFSpace.java new file mode 100644 index 000000000..a68c35897 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFSpace.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test.mocks; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.mockito.Mockito; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFOrganization; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFServiceInstance; +import org.springsource.ide.eclipse.commons.livexp.core.LiveCounter; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +public class MockCFSpace extends CFSpaceData { + + //TODO: the methods in this class should prolly be synchronized somehow. It manipulates mutable + // data and is called from multiple threads. + + private Map servicesByName = new HashMap<>(); + private Map appsByName = new HashMap<>(); + private MockCloudFoundryClientFactory owner; + private Map pushCounts = new HashMap<>(); + + public MockCFSpace(MockCloudFoundryClientFactory owner, String name, UUID guid, CFOrganization org) { + super(name, guid, org); + this.owner = owner; + } + + public String getDefaultDomain() { + return owner.getDefaultDomain(); + } + + public List getServices() { + return ImmutableList.copyOf(servicesByName.values()); + } + + public ImmutableList getApplicationsWithBasicInfo() { + Builder builder = ImmutableList.builder(); + for (MockCFApplication app : appsByName.values()) { + builder.add(app.getBasicInfo()); + } + return builder.build(); + } + + public MockCFApplication defApp(String name) { + MockCFApplication existing = appsByName.get(name); + if (existing==null) { + appsByName.put(name, existing = Mockito.spy(new MockCFApplication(owner, this, name))); + } + return existing; + } + + public CFServiceInstance defService(String name) { + CFServiceInstance existing = servicesByName.get(name); + if (existing==null) { + servicesByName.put(name, new CFServiceInstanceData( + name + )); + } + return existing; + } + + public MockCFApplication getApplication(UUID appGuid) { + for (MockCFApplication app : appsByName.values()) { + if (app.getGuid().equals(appGuid)) { + return app; + } + } + return null; + } + + public MockCFApplication getApplication(String appName) { + MockCFApplication app = appsByName.get(appName); + if (app!=null) { + return app; + } + return null; + } + + + public boolean removeApp(String name) { + return appsByName.remove(name)!=null; + } + + public void put(MockCFApplication app) { + appsByName.put(app.getName(), app); + } + + public synchronized LiveCounter getPushCount(String name) { + LiveCounter counter = pushCounts.get(name); + if (counter==null) { + pushCounts.put(name, counter = new LiveCounter()); + } + return counter; + } + + public void deleteService(String serviceName) throws Exception { + CFServiceInstance x = servicesByName.remove(serviceName); + if (x==null) { + throw new IOException("Service doesn't exist: "+serviceName); + } + } + + @Override + public String toString() { + return "MockCFSpace("+getName()+")"; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFStack.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFStack.java new file mode 100644 index 000000000..d25bdc14f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCFStack.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test.mocks; + +import org.springframework.ide.eclipse.boot.dash.cf.client.CFStack; + +/** + * @author Kris De Volder + */ +public class MockCFStack implements CFStack { + + private String name; + + public MockCFStack(String name) { + super(); + this.name = name; + } + + @Override + public String getName() { + return name; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCloudFoundryClientFactory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCloudFoundryClientFactory.java new file mode 100644 index 000000000..ff6b0c23a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockCloudFoundryClientFactory.java @@ -0,0 +1,667 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test.mocks; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.apache.commons.lang.RandomStringUtils; +import org.cloudfoundry.client.CloudFoundryClient; +import org.osgi.framework.Version; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplication; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFApplicationDetail; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFBuildpack; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFClientParams; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFDomainType; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFOrganization; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFServiceInstance; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFSpace; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFStack; +import org.springframework.ide.eclipse.boot.dash.cf.client.ClientRequests; +import org.springframework.ide.eclipse.boot.dash.cf.client.CloudFoundryClientFactory; +import org.springframework.ide.eclipse.boot.dash.cf.client.SshClientSupport; +import org.springframework.ide.eclipse.boot.dash.cf.client.SshHost; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCredentials.CFCredentialType; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFCloudDomainData; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFDomainStatus; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFPushArguments; +import org.springframework.ide.eclipse.boot.dash.cf.routes.ParsedUri; +import org.springframework.ide.eclipse.boot.dash.cf.routes.RouteBinding; +import org.springframework.ide.eclipse.boot.dash.console.IApplicationLogConsole; +import org.springframework.ide.eclipse.boot.dash.test.CfTestTargetParams; +import org.springframework.ide.eclipse.boot.dash.test.util.LiveExpToFlux; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import junit.framework.Assert; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class MockCloudFoundryClientFactory extends CloudFoundryClientFactory { + + private static void debug(String string) { + System.out.println(string); + } + + private final Set instances = Collections.synchronizedSet(new HashSet<>()); + + public static final String FAKE_REFRESH_TOKEN = "fakeRefreshToken"; + public static final String FAKE_PASSWORD = CfTestTargetParams.fromEnv("CF_TEST_PASSWORD"); + private Version supportedApiVersion = new Version(CloudFoundryClient.SUPPORTED_API_VERSION); + private Version apiVersion = supportedApiVersion; + + private Map orgsByName = new LinkedHashMap<>(); + private Map spacesByName = new LinkedHashMap<>(); + private Map domainsByName = new LinkedHashMap<>(); + private Map buildpacksByName = new LinkedHashMap<>(); + private Map stacksByName = new LinkedHashMap<>(); + + private Set ssoTokens = new HashSet<>(); + + /** + * Becomes non-null if notImplementedStub is called, used to check that the tests + * only use parts of the mocking harness that are actually implemented. + */ + private Exception notImplementedStubCalled = null; + private long startDelay = 0; + + public MockCloudFoundryClientFactory() { + defDomain("cfmockapps.io"); //Lost of functionality may assume there's at least one domain so make sure we have one. + defBuildpacks("java-buildpack", "ruby-buildpack", "funky-buildpack", "another-buildpack"); + defStacks("cflinuxfs2", "windows2012R2"); + } + + synchronized public String getSsoToken() { + String token = RandomStringUtils.randomAlphabetic(8); + ssoTokens.add(token); + return token; + } + + /** + * Verfies the validity of a sso token. Sso token can only be used once + * so this check implicitly invalidates the token. + * + * @return Whether the token was valid prior to the call to this method. + */ + private synchronized boolean checkSsoToken(String token) { + return ssoTokens.remove(token); + } + + public void defStacks(String... names) { + for (String n : names) { + defStack(n); + } + } + + public MockCFStack defStack(String name) { + MockCFStack stack = new MockCFStack(name); + stacksByName.put(name, stack); + return stack; + } + + @Override + public ClientRequests getClient(CFClientParams params) { + return new MockClient(params); + } + + public CFCloudDomain defDomain(String name) { + CFCloudDomainData it = new CFCloudDomainData(name); + domainsByName.put(name, it); + return it; + } + + public CFCloudDomain defDomain(String name, CFDomainType type, CFDomainStatus status) { + CFCloudDomainData it = new CFCloudDomainData(name, type, status); + domainsByName.put(name, it); + return it; + } + + public String getDefaultDomain() { + return domainsByName.keySet().iterator().next(); + } + + public MockCFSpace defSpace(String orgName, String spaceName) { + String key = orgName+"/"+spaceName; + MockCFSpace existing = spacesByName.get(key); + if (existing==null) { + CFOrganization org = defOrg(orgName); + spacesByName.put(key, existing= new MockCFSpace(this, + spaceName, + UUID.randomUUID(), + org + )); + } + return existing; + } + + public CFOrganization defOrg(String orgName) { + CFOrganization existing = orgsByName.get(orgName); + if (existing==null) { + orgsByName.put(orgName, existing = new CFOrganizationData( + orgName, + UUID.randomUUID() + )); + } + return existing; + } + + public void assertOnlyImplementedStubsCalled() throws Exception { + if (notImplementedStubCalled!=null) { + throw notImplementedStubCalled; + } + } + + private String validPassword = FAKE_PASSWORD; + /** + * Change the current password this mock client will accept when trying to + * use password-based authentication. + */ + public void setPassword(String newPassword) { + this.validPassword = newPassword; + } + + private class MockClient implements ClientRequests { + + private class MockSshClientSupport implements SshClientSupport { + + @Override + public SshHost getSshHost() throws Exception { + return new SshHost("ssh.host.somewhere", 2222, "some-ssh-fingerprint"); + } + + @Override + public String getSshUser(String appName, int instance) throws Exception { + MockCFApplication app = getSpace().getApplication(appName); + if (app==null) { + throw new IOException("App not found"); + } + UUID guid = app.getGuid(); + Assert.assertNotNull(guid); + return getSshUser(guid, instance); + } + + @Override + public String getSshCode() throws Exception { + return "an-ssh-code"; + } + + @Override + public String getSshUser(UUID appGuid, int instance) throws Exception { + return appGuid+"/"+instance; + } + + } + + private int nextPort = 63000; + private synchronized int choosePort() { + return nextPort++; + } + + private CFClientParams params; + private boolean connected = true; + private Boolean validCredentials = null; + + + private final LiveVariable refreshToken = new LiveVariable<>(); + + public MockClient(CFClientParams params) { + this.params = params; + instances.add(this); + debug("created Mock CF Client: "+instances.size()); + refreshToken.addListener((e,v) -> debug("refreshToken <- "+v)); + refreshToken.onDispose(d -> debug("refreshToken DISPOSED")); + } + + private void notImplementedStub() { + IllegalStateException e = new IllegalStateException("CF Client Stub Not Yet Implemented"); + if (notImplementedStubCalled==null) { + notImplementedStubCalled = e; + } + throw e; + } + + @Override + public Flux getApplicationDetails(List appsToLookUp) throws Exception { + checkConnection(); + MockCFSpace space = getSpace(); + return Flux.fromIterable(appsToLookUp) + .flatMap((app) -> { + return Mono.justOrEmpty(space.getApplication(app.getGuid()).getDetailedInfo()); + }); + } + + @Override + public Disposable streamLogs(String appName, IApplicationLogConsole logConsole) throws Exception { + checkConnection(); + //TODO: This 'log streamer' is a total dummy for now. It doesn't stream any data and canceling it does nothing. + return Flux.empty().subscribe(); + } + + @Override + public void stopApplication(String appName) throws Exception { + checkConnection(); + MockCFApplication app = getSpace().getApplication(appName); + if (app==null) { + throw errorAppNotFound(appName); + } + app.stop(); + } + + @Override + public void restartApplication(String appName, CancelationToken cancelationToken) throws Exception { + checkConnection(); + MockCFApplication app = getSpace().getApplication(appName); + if (app==null) { + throw errorAppNotFound(appName); + } + app.restart(cancelationToken); + } + + @Override + public void dispose() { + connected = false; + refreshToken.dispose(); + instances.remove(this); + debug("Mock CF Client disposed: "+instances.size()); + } + + @Override + public SshClientSupport getSshClientSupport() throws Exception { + return new MockSshClientSupport(); + } + + @SuppressWarnings("unchecked") + @Override + public List getSpaces() throws Exception { + checkConnection(); + @SuppressWarnings("rawtypes") + List hack = ImmutableList.copyOf(spacesByName.values()); + return hack; + } + + @Override + public List getServices() throws Exception { + checkConnection(); + return getSpace().getServices(); + } + + private MockCFSpace getSpace() throws IOException { + if (params.getOrgName()==null) { + throw errorNoOrgSelected(); + } + if (params.getSpaceName()==null) { + throw errorNoSpaceSelected(); + } + MockCFSpace space = spacesByName.get(params.getOrgName()+"/"+params.getSpaceName()); + if (space==null) { + throw errorSpaceNotFound(params.getOrgName()+"/"+params.getSpaceName()); + } + return space; + } + + @Override + public List getDomains() throws Exception { + checkConnection(); + return ImmutableList.copyOf(domainsByName.values()); + } + + @Override + public List getBuildpacks() throws Exception { + checkConnection(); + return ImmutableList.copyOf(buildpacksByName.values()); + } + + @Override + public List getApplicationsWithBasicInfo() throws Exception { + checkConnection(); + return getSpace().getApplicationsWithBasicInfo(); + } + + @Override + public CFApplicationDetail getApplication(String appName) throws Exception { + checkConnection(); + MockCFApplication app = getSpace().getApplication(appName); + if (app!=null) { + return app.getDetailedInfo(); + } + return null; + } + + @Override + public Version getApiVersion() { + return apiVersion; + } + + @Override + public Version getSupportedApiVersion() { + return supportedApiVersion; + } + + @Override + public void deleteApplication(String name) throws Exception { + checkConnection(); + if (!getSpace().removeApp(name)) { + throw errorAppNotFound(name); + } + } + + @Override + public String getHealthCheck(UUID appGuid) throws Exception { + checkConnection(); + MockCFApplication app = getApplication(appGuid); + if (app == null) { + throw errorAppNotFound("GUID: "+appGuid.toString()); + } else { + return app.getHealthCheckType(); + } + } + + private MockCFApplication getApplication(UUID appGuid) throws IOException { + return getSpace().getApplication(appGuid); + } + + /** + * Each mock operation that does something requires access to CF should call this + * to ensure that it implicitly check whether the connection is valid. + *

+ * Operations on 'invalid' connection are expected to throw Exceptions. + * Calling this method makes the operations behave as expected. For example, + * fail when logged out, or when connection was created with invalid credentials. + */ + private void checkConnection() throws Exception { + if (!connected) { + throw errorClientNotConnected(); + } + if (validCredentials==null) { + validCredentials = isValidCredentials(params.getUsername(), params.getCredentials()); + } + if (!validCredentials) { + throw errorInvalidCredentials(); + } + } + + private boolean isValidCredentials(String username, CFCredentials credentials) throws Exception { + CFCredentialType type = credentials.getType(); + String secret = credentials.getSecret(); + if (type==CFCredentialType.PASSWORD) { + if (!credentials.getSecret().equals(validPassword)) { + return false; + } + } else if (type==CFCredentialType.REFRESH_TOKEN) { + if (!secret.equals(FAKE_REFRESH_TOKEN)) { + return false; + } + } else if (type==CFCredentialType.TEMPORARY_CODE) { + if (!checkSsoToken(secret)) { + return false; + } + } else { + return false; + } + //Validation of credentials is expected to update refresh token. + refreshToken.setValue(FAKE_REFRESH_TOKEN); + return true; + } + + @Override + public void setHealthCheck(UUID guid, String hcType) throws Exception { + checkConnection(); + notImplementedStub(); + } + + @Override + public List getStacks() throws Exception { + checkConnection(); + return ImmutableList.copyOf(stacksByName.values()); + } + + @Override + public boolean applicationExists(String appName) throws Exception { + checkConnection(); + return getSpace().getApplication(appName) !=null; + } + + @Override + public void push(CFPushArguments args, CancelationToken cancelationToken) throws Exception { + checkConnection(); + System.out.println("Pushing: "+args); + //TODO: should check services exist and raise an error because non-existant services cannot be bound. + MockCFSpace space = getSpace(); + MockCFApplication app = new MockCFApplication(MockCloudFoundryClientFactory.this, space, args.getAppName()); + app.setBuildpackUrlMaybe(args.getBuildpack()); + app.setRoutes(buildRoutes(args)); + + app.setCommandMaybe(args.getCommand()); + app.setDiskQuotaMaybe(args.getDiskQuota()); + app.setEnvMaybe(args.getEnv()); + app.setMemoryMaybe(args.getMemory()); + app.setServicesMaybe(args.getServices()); + app.setStackMaybe(args.getStack()); + app.setTimeoutMaybe(args.getTimeout()); + app.setHealthCheckTypeMaybe(args.getHealthCheckType()); + app.setHealthCheckHttpEndpoint(args.getHealthCheckHttpEndpoint()); + app.setBits(() -> { + try { + return Files.readAllBytes(args.getApplicationDataAsFile().toPath()); + } catch (IOException e) { + return new byte[0]; + } + }); + space.put(app); + space.getPushCount(app.getName()).increment(); + + app.start(cancelationToken); + } + + private Collection buildRoutes(CFPushArguments args) { + List desiredUris = args.getRoutes(); + if (desiredUris!=null) { + return desiredUris.stream() + .map(uri -> buildRoute(uri, args)) + .collect(Collectors.toList()); + } + return ImmutableList.of(); + } + + private RouteBinding buildRoute(String _uri, CFPushArguments args) { + ParsedUri uri = new ParsedUri(_uri); + boolean randomRoute = args.getRandomRoute(); + CFCloudDomainData bestDomain = domainsByName.values().stream() + .filter(domain -> domainCanBeUsedFor(domain, uri)) + .max((d1, d2) -> Integer.compare(d1.getName().length(), d2.getName().length())) + .orElse(null); + if (bestDomain==null) { + throw new IllegalStateException("No domain matching the given uri '"+_uri+"' could be found"); + } + RouteBinding route = new RouteBinding(); + route.setDomain(bestDomain.getName()); + route.setHost(bestDomain.splitHost(uri.getHostAndDomain())); + route.setPath(uri.getPath()); + route.setPort(uri.getPort()); + if (randomRoute) { + if (bestDomain.getType()==CFDomainType.TCP) { + if (route.getPort()==null) { + route.setPort(choosePort()); + } + } else if (bestDomain.getType()==CFDomainType.HTTP) { + if (route.getHost()==null) { + route.setHost(chooseHost()); + } + } + } + return route; + } + + private String chooseHost() { + return RandomStringUtils.randomAlphabetic(8).toLowerCase(); + } + + private boolean domainCanBeUsedFor(CFCloudDomainData domainData, ParsedUri uri) { + String domain = domainData.getName(); + String hostAndDomain = uri.getHostAndDomain(); + String host; + if (!hostAndDomain.endsWith(domain)) { + return false; + } + if (domain.length()==hostAndDomain.length()) { + //The uri matches domain precisely + host = null; + } else if (hostAndDomain.charAt(hostAndDomain.length()-domain.length()-1)=='.') { + //THe uri matches as ${host}.${domain} + host = hostAndDomain.substring(0, hostAndDomain.length()-domain.length()-1); + } else { + //Couldn't match this domain to uri + return false; + } + if (domainData.getType()==CFDomainType.TCP) { + return host==null; //TCP routes don't allow setting a host, only a port + } else if (domainData.getType()==CFDomainType.HTTP) { + return uri.getPort()==null; //HTTP routes don't allow setting a port only a host + } else { + throw new IllegalStateException("Unknown domain type: "+domainData.getType()); + } + } + + @Override + public Map getApplicationEnvironment(String appName) throws Exception { + checkConnection(); + MockCFApplication app = getSpace().getApplication(appName); + if (app==null) { + throw errorAppNotFound(appName); + } + return ImmutableMap.copyOf(app.getEnv()); + } + + @Override + public Mono deleteServiceAsync(String serviceName) { + return Mono.defer(() -> { + try { + checkConnection(); + getSpace().deleteService(serviceName); + return Mono.empty(); + } catch (Exception e) { + return Mono.error(e); + } + }); + } + + @Override + public Mono getUserName() { + return Mono.defer(() -> { + try { + checkConnection(); + return Mono.just(params.getUsername()); + } catch (Exception e) { + return Mono.error(e); + } + }); + } + + @Override + public String getRefreshToken() { + return refreshToken.getValue(); + } + + @Override + public Flux getRefreshTokens() { + return LiveExpToFlux.toFlux(refreshToken); + } + } + + public void defBuildpacks(String... names) { + for (String n : names) { + defBuildpack(n); + } + } + + public MockCFBuildpack defBuildpack(String n) { + MockCFBuildpack it = new MockCFBuildpack(n); + buildpacksByName.put(n, it); + return it; + } + + ////////////////////////////////////////////////// + // Exception creation methods + + protected IOException errorAppNotFound(String detailMessage) throws IOException { + return new IOException("App not found: "+detailMessage); + } + + protected IOException errorClientNotConnected() { + return new IOException("CF Client not Connected"); + } + + protected IOException errorNoOrgSelected() { + return new IOException("No org selected"); + } + + protected IOException errorNoSpaceSelected() { + return new IOException("No space selected"); + } + + protected IOException errorSpaceNotFound(String detail) { + return new IOException("Space not found: "+detail); + } + + protected IOException errorAppAlreadyExists(String detail) { + return new IOException("App already exists: "+detail); + } + + protected Exception errorInvalidCredentials() { + return new Exception("Cannot connect to CF. Invalid credentials."); + } + + public void setAppStartDelay(TimeUnit timeUnit, int howMany) { + startDelay = timeUnit.toMillis(howMany); + } + + /** + * @return The delay that a simulated 'start' of an app should take before returning. Given in milliseconds. + */ + public long getStartDelay() { + return startDelay; + } + + public void setApiVersion(String string) { + apiVersion = new Version(string); + } + + public void setSupportedApiVersion(String string) { + supportedApiVersion = new Version(string); + } + + public int instanceCount() { + return instances.size(); + } + + public void changeRefrestToken(String newToken) { + for (MockClient client : instances) { + client.refreshToken.setValue(newToken); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockLiveProcesses.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockLiveProcesses.java new file mode 100644 index 000000000..4984f899d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockLiveProcesses.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * 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.test.mocks; + +import static org.junit.Assert.assertEquals; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.eclipse.core.resources.IProject; +import org.junit.Assert; +import org.springframework.ide.eclipse.boot.dash.liveprocess.CommandInfo; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveProcessCommandsExecutor; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveProcessCommandsExecutor.Server; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class MockLiveProcesses { + + public static final String COMMAND_CONNECT = "sts/livedata/connect"; + public static final String COMMAND_REFRESH = "sts/livedata/refresh"; + public static final String COMMAND_DISCONNECT = "sts/livedata/disconnect"; + + private final Map processes = new LinkedHashMap<>(); + + public class MockProcess { + private int refreshCount = 0; + private boolean connected = false; + private ImmutableMap processData; + + private MockProcess(ImmutableMap processData) { + this.processData = processData; + Assert.assertTrue(processData.containsKey("processKey")); + String processKey = processData.get("processKey"); + Assert.assertFalse("duplicate process key", processes.containsKey(processKey)); + processes.put(processKey, this); + } + + public int getRefreshCount() { + return refreshCount; + } + + public boolean isConnected() { + return connected; + } + + public void connect() { + connected = true; + refreshCount = 0; + } + + public void refresh() { + refreshCount++; + } + + public void disconnect() { + connected = false; + } + + public void assertRefreshed(int expectedCount) { + assertEquals(expectedCount, refreshCount); + } + + public String getProcessKey() { + return processData.get("processKey"); + } + + public Map getInfo() { + return processData; + } + } + + + private final Server server = new Server() { + + @Override + public Flux listCommands() { + return Flux.fromIterable(processes.values()) + .flatMapIterable(this::createCommands); + } + + private List createCommands(MockProcess process) { + if (process.isConnected()) { + return ImmutableList.of( + command(COMMAND_DISCONNECT, process), + command(COMMAND_REFRESH, process) + ); + } else { + return ImmutableList.of(command(COMMAND_CONNECT, process)); + } + } + + private CommandInfo command(String cmdId, MockProcess process) { + ImmutableMap.Builder info = ImmutableMap.builder(); + info.put("action", cmdId); + info.putAll(process.getInfo()); + return new CommandInfo(cmdId, info.build()); + } + + @Override + public Mono executeCommand(CommandInfo cmd) { + return Mono.fromRunnable(() -> { + MockProcess process = processes.get(cmd.info.get("processKey")); + switch (cmd.command) { + case COMMAND_DISCONNECT: + process.disconnect(); + break; + case COMMAND_CONNECT: + process.connect(); + break; + case COMMAND_REFRESH: + process.refresh(); + break; + default: + break; + } + }); + } + }; + + public LiveProcessCommandsExecutor commandExecutor = new LiveProcessCommandsExecutor() { + @Override + public List getLanguageServers() { + return ImmutableList.of(server); + } + }; + + public MockProcess newLocalProcess(String processKey, IProject project, int pid) { + if (project!=null) { + return new MockProcess(ImmutableMap.of( + "processKey", processKey, + "projectName", project.getName(), + "processId", ""+pid, + "label", processKey +" lbl" + )); + } else { + return new MockProcess(ImmutableMap.of( + "processKey", processKey, + "processId", ""+pid, + "label", processKey +" lbl" + )); + } + } + + public MockProcess newProcess(ImmutableMap processData) { + return new MockProcess(processData); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockMultiSelection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockMultiSelection.java new file mode 100644 index 000000000..fd100f4b0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockMultiSelection.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test.mocks; + +import java.util.Collection; + +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; + +import com.google.common.collect.ImmutableSet; + +/** + * Provides a {@link MultiSelection} and some convenience methods to change + * and get the selected elements in test code. + * + * @author Kris De Volder + */ +public class MockMultiSelection { + + private final MultiSelection readableSelection; + private final LiveSetVariable writableSelection = new LiveSetVariable<>(AsyncMode.SYNC); + + public MockMultiSelection(Class klass) { + readableSelection = new MultiSelection<>(klass, writableSelection); + } + + @SuppressWarnings("unchecked") + public void setElements(T... newElements) { + setElements(ImmutableSet.copyOf(newElements)); + } + + public void setElements(Collection newElements) { + writableSelection.replaceAll(newElements); + } + + public MultiSelection forReading() { + return readableSelection; + } + + public boolean isEmpty() { + return readableSelection.getValue().isEmpty(); + } + + public ImmutableSet getElements() { + return readableSelection.getElements().getValues(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockRunTarget.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockRunTarget.java new file mode 100644 index 000000000..62cebf9f0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockRunTarget.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2015, 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test.mocks; + +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.DEFAULT_PATH; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.INSTANCES; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.NAME; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.PROJECT; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.RUN_STATE_ICN; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.TAGS; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryTargetProperties; +import org.springframework.ide.eclipse.boot.dash.model.AbstractRunTarget; +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.RunTargetWithProperties; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.TargetProperties; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; + +public class MockRunTarget extends AbstractRunTarget implements RunTargetWithProperties { + + private CloudFoundryTargetProperties properties; + private boolean requiresCredentials; + + public MockRunTarget(RunTargetType type, CloudFoundryTargetProperties properties) { + this(type, properties, false); + } + + public MockRunTarget(RunTargetType type, CloudFoundryTargetProperties properties, boolean requiresCredentials) { + super(type, properties.getRunTargetId()); + this.properties = properties; + this.requiresCredentials = requiresCredentials; + } + + private final BootDashColumn[] defaultColumns = { RUN_STATE_ICN, NAME, PROJECT, INSTANCES, DEFAULT_PATH, TAGS }; + + @Override + public BootDashColumn[] getDefaultColumns() { + return defaultColumns; + } + + @Override + public MockBootDashModel createSectionModel(BootDashViewModel viewModel) { + return new MockBootDashModel(this, viewModel.getContext(), viewModel); + } + + @Override + public boolean canRemove() { + return true; + } + + @Override + public boolean canDeployAppsFrom() { + return false; + } + + public String get(String prop) { + return properties.get(prop); + } + + @Override + public TargetProperties getTargetProperties() { + return properties; + } + + @Override + public boolean requiresCredentials() { + return requiresCredentials; + } + + @Override + public CloudFoundryTargetProperties getParams() { + return properties; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockRunTargetType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockRunTargetType.java new file mode 100644 index 000000000..1268991ff --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockRunTargetType.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test.mocks; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.springframework.ide.eclipse.boot.dash.cf.runtarget.CloudFoundryTargetProperties; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModelContext; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.AbstractRunTargetType; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.TargetProperties; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; + +import com.google.gson.Gson; + +public class MockRunTargetType extends AbstractRunTargetType { + + private boolean requiresCredentials; + + public MockRunTargetType(SimpleDIContext injections, String name) { + super(injections, name); + } + + @Override + public boolean canInstantiate() { + return true; + } + + @Override + public CompletableFuture openTargetCreationUi(LiveSetVariable targets) { + return CompletableFuture.completedFuture(null); + } + + @Override + public RunTarget createRunTarget(CloudFoundryTargetProperties properties) { + return new MockRunTarget(this, properties, requiresCredentials); + } + + @Override + public ImageDescriptor getIcon() { + return null; + } + + public void setRequiresCredentials(boolean requires) { + this.requiresCredentials = requires; + } + + @Override + public CloudFoundryTargetProperties parseParams(String serializedTargetParams) { + Gson gson = new Gson(); + @SuppressWarnings("unchecked") + Map map = gson.fromJson(serializedTargetParams, Map.class); + return new CloudFoundryTargetProperties(new TargetProperties(map, this), this, injections); + } + + @Override + public String serialize(CloudFoundryTargetProperties props) { + return props.toJson(); + } + + @Override + public ImageDescriptor getDisconnectedIcon() { + return null; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockRunnableContext.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockRunnableContext.java new file mode 100644 index 000000000..264b3e7ec --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockRunnableContext.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test.mocks; + +import java.lang.reflect.InvocationTargetException; + +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jface.operation.IRunnableContext; +import org.eclipse.jface.operation.IRunnableWithProgress; + +public class MockRunnableContext implements IRunnableContext { + + @Override + public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable) throws InvocationTargetException, InterruptedException { + //TODO: we are ignoring the 'fork' flag. Is that bad? + runnable.run(new NullProgressMonitor()); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockScopedPropertyStore.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockScopedPropertyStore.java new file mode 100644 index 000000000..5e1c616c4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockScopedPropertyStore.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2015 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.test.mocks; + +import java.util.HashMap; +import java.util.Map; + +import org.springsource.ide.eclipse.commons.core.pstore.IScopedPropertyStore; + +/** + * A memory-backed {@link IScopedPropertyStore} suitable for testing. + * + * @author Kris De Volder + */ +public class MockScopedPropertyStore implements IScopedPropertyStore { + + private Map store = new HashMap<>(); + + @Override + public String get(T element, String key) { + return store.get(new Key(element, key)); + } + + @Override + public void put(T element, String key, String value) throws Exception { + store.put(new Key(element, key), value); + } + + + private class Key { + public final T element; + public final String key; + public Key(T element, String key) { + this.element = element; + this.key = key; + } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + getOuterType().hashCode(); + result = prime * result + ((element == null) ? 0 : element.hashCode()); + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Key other = (Key) obj; + if (!getOuterType().equals(other.getOuterType())) + return false; + if (element == null) { + if (other.element != null) + return false; + } else if (!element.equals(other.element)) + return false; + if (key == null) { + if (other.key != null) + return false; + } else if (!key.equals(other.key)) + return false; + return true; + } + private MockScopedPropertyStore getOuterType() { + return MockScopedPropertyStore.this; + } + + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockSecuredCredentialStore.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockSecuredCredentialStore.java new file mode 100644 index 000000000..e2ef66f9a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockSecuredCredentialStore.java @@ -0,0 +1,30 @@ +package org.springframework.ide.eclipse.boot.dash.test.mocks; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.ide.eclipse.boot.dash.model.SecuredCredentialsStore; + +public class MockSecuredCredentialStore implements SecuredCredentialsStore { + + private Map store = new HashMap<>(); + private boolean isUnlocked = false; + + @Override + public synchronized String getCredentials(String runTargetId) { + isUnlocked = true; + return store.get(runTargetId); + } + + @Override + public synchronized void setCredentials(String runTargetId, String password) { + isUnlocked = true; + store.put(runTargetId, password); + } + + @Override + public boolean isUnlocked() { + return isUnlocked; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockSshTunnel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockSshTunnel.java new file mode 100644 index 000000000..1e3ca0c5d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/MockSshTunnel.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2018 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.test.mocks; + +import org.springframework.ide.eclipse.boot.dash.cf.client.SshHost; +import org.springframework.ide.eclipse.boot.dash.cf.debug.SshTunnel; +import org.springframework.ide.eclipse.boot.dash.util.LogSink; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; + +public class MockSshTunnel extends AbstractDisposable implements SshTunnel { + + boolean disposed = false; + private SshHost sshHost; + private String user; + private String oneTimeCode; + private int remotePort; + private LogSink log; + private int localPort; + + public MockSshTunnel( + SshHost sshHost, + String user, + String oneTimeCode, + int remotePort, + LogSink log, + int localPort + ) { + this.sshHost = sshHost; + this.user = user; + this.oneTimeCode = oneTimeCode; + this.remotePort = remotePort; + this.log = log; + this.localPort = localPort; + } + + public boolean isDisposed() { + return disposed; + } + + public SshHost getSshHost() { + return sshHost; + } + + public String getUser() { + return user; + } + + public String getOneTimeCode() { + return oneTimeCode; + } + + public int getRemotePort() { + return remotePort; + } + + public LogSink getLog() { + return log; + } + + public int getLocalPort() { + return localPort; + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/Mocks.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/Mocks.java new file mode 100644 index 000000000..d6f161517 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/Mocks.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2015 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test.mocks; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.eclipse.core.resources.IProject; + +public class Mocks { + + public static IProject mockProject(String name, boolean exists) { + IProject project = mock(IProject.class); + when(project.getName()).thenReturn(name); + when(project.exists()).thenReturn(exists); + return project; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/RunStateHistory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/RunStateHistory.java new file mode 100644 index 000000000..129e8c956 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/mocks/RunStateHistory.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2016 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.test.mocks; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +import com.google.common.collect.ImmutableList; + +public class RunStateHistory implements ValueListener { + + List history = new ArrayList<>(); + + @Override + public void gotValue(LiveExpression exp, RunState value) { + history.add(value); + } + + public void assertHistory(RunState... runStates) { + assertEquals(ImmutableList.copyOf(runStates), ImmutableList.copyOf(history)); + } + + public void assertHistoryContains(RunState... expectedStates) { + for (RunState runState : expectedStates) { + if (!history.contains(runState)) { + fail("Not found: "+runState+" in "+expectedStates); + } + } + } + + public void assertLast(RunState expectedState) { + assertEquals(expectedState, history.get(history.size()-1)); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/JobUtil.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/JobUtil.java new file mode 100644 index 000000000..f031125f9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/JobUtil.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test.util; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +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; + +public class JobUtil { + + public static Future runInJob(Callable work) { + CompletableFuture done = new CompletableFuture<>(); + Job job = new Job("Backgorund Work") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + done.complete(work.call()); + } catch (Throwable e) { + done.completeExceptionally(e); + } + return Status.OK_STATUS; + } + }; + job.schedule(); + return done; + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/LiveExpToFlux.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/LiveExpToFlux.java new file mode 100644 index 000000000..13d104c86 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/LiveExpToFlux.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test.util; + +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +import reactor.core.publisher.Flux; + +public class LiveExpToFlux { + +// private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder"); +// +// private static void debug(String string) { +// if (DEBUG) { +// System.out.println(string); +// } +// } + + /** + * Convert a LiveExp into a Flux. The flux delivers all non-null livexp values in its + * `onNext` event. The current value is sent immediately upon subscribing and subsequent + * `onNext` fire whenever the value changes. + *

+ * The flux completes normally when the targetted LiveExp is disposed. (This means that + * when a LiveExp is already disposed, susbcribers will receive 'onComplete' event immediately. + * I.e. the subcriber will perceive the stream of livexp values as a empty stream. + */ + public static Flux toFlux(LiveExpression exp) { + return Flux.create(sink -> { +// debug("Creating LiveExpFlux"); + ValueListener valueListener = (e, v) -> { +// debug("LiveExpFlux <- "+v); + if (v!=null) { + sink.next(v); + } + }; + sink.onDispose(() -> exp.removeListener(valueListener)); + exp.addListener(valueListener); + exp.onDispose((d) -> { +// debug("LiveExpFlux LiveExp DISPOSED"); + sink.complete(); + }); + }); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/SslValidationDisabler.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/SslValidationDisabler.java new file mode 100644 index 000000000..be50d02c5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/SslValidationDisabler.java @@ -0,0 +1,56 @@ +package org.springframework.ide.eclipse.boot.dash.test.util; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +public class SslValidationDisabler { + + private static boolean installed = false; + + public synchronized static void disableSslValidation() throws Exception { + if (installed) { + return; + } + installed = true; + // Create a trust manager that does not validate certificate chains + TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() { + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + }}; + + // Install the all-trusting trust manager + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + + // Create all-trusting host name verifier + HostnameVerifier allHostsValid = new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; + + // Install the all-trusting host verifier + HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/ZipDiff.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/ZipDiff.java new file mode 100644 index 000000000..69166019e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/util/ZipDiff.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.test.util; + +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Utility for comparing a given 'expected' zip file's contents with + * a 'actual' zip contents. + *

+ * Two zips are considered equal if they have the same files / folders + * in their table-of contents and each of the files has identical contents. + *

+ * This differ doesn't actually compare two files directly but rather computes + * a hash of the file contents instead and compares the hashes. So... + * their is a small chance that a difference may not be detected. + * + * @author Kris De Volder + */ +public class ZipDiff { + + final Map expectedHashes; + + public ZipDiff(InputStream expectedZipData) throws IOException { + expectedHashes = computeHashes(expectedZipData); + } + + public Map computeHashes(InputStream expectedZipData) throws IOException { + Map hashes = new HashMap<>(); + ZipInputStream zip = new ZipInputStream(expectedZipData); + ZipEntry ze; + byte[] buffer = new byte[1024]; + while (null != (ze = zip.getNextEntry())) { + String path = ze.getName(); + if (ze.isDirectory()) { + hashes.put(path, -1L); + } else { + CRC32 hasher = new CRC32(); + int len; + while ((len = zip.read(buffer)) > 0) { + hasher.update(buffer, 0, len); + } + hashes.put(path, hasher.getValue()); + } + } + return hashes; + } + + /** + * Read data from 'actualBits', interpret is as a ZipInputStream, + * and compare its contents to the expected contents. + *

+ * Throws an exception when a difference is found. + */ + public void assertEqual(InputStream actualBits) throws IOException { + Map actualHashes = computeHashes(actualBits); + + //Check that everything that is in 'actualBits' was as expected + for (Entry actual : actualHashes.entrySet()) { + if (!expectedHashes.containsKey(actual.getKey())) { + fail("ZipEntry found but not expected: "+actual.getKey()); + } + long expectedHash = expectedHashes.get(actual.getKey()); + long actualHash = actual.getValue(); + if (expectedHash!=actualHash) { + fail("ZipEntry with different hashes: "+actual.getKey()+" "+expectedHash+"!="+actualHash); + } + } + + //Check that there's nothing that was expected but was missing from 'actual' + for (String expected : expectedHashes.keySet()) { + if (!actualHashes.containsKey(expected)) { + fail("ZipEntry expected but not found: "+expected); + } + } + } + + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/AppNameReconcilerTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/AppNameReconcilerTest.java new file mode 100644 index 000000000..4fe41ab0a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/AppNameReconcilerTest.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2016, 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.test.yaml; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; + +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jface.text.Document; +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.AppNameAnnotation; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.AppNameAnnotationModel; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.AppNameReconciler; +import org.springframework.ide.eclipse.editor.support.yaml.ast.YamlASTProvider; +import org.yaml.snakeyaml.Yaml; + +/** + * Tests for application name reconciler + * + * @author Alex Boyko + * + */ +public class AppNameReconcilerTest { + + private void testAppNames(String manifest, String...expectedAppNames) throws Exception { + AppNameReconciler reconciler = new AppNameReconciler(new YamlASTProvider(new Yaml())); + AppNameAnnotationModel annotationModel = new AppNameAnnotationModel("test"); + Document doc = new Document(manifest); + reconciler.reconcile(doc, annotationModel, new NullProgressMonitor()); + HashSet actualAppNames = new HashSet<>(); + for (Iterator itr = annotationModel.getAnnotationIterator(); itr.hasNext();) { + Object o = itr.next(); + if (o instanceof AppNameAnnotation) { + actualAppNames.add(((AppNameAnnotation)o).getText()); + } + } + assertEquals(new HashSet<>(Arrays.asList(expectedAppNames)), actualAppNames); + } + + @Test + public void testSingleAppName() throws Exception { + testAppNames( + "applications:\n" + + "- name: app\n" + + " memory: 512M\n", + "app"); + } + + @Test + public void testMultipleAppNames() throws Exception { + testAppNames( + "applications:\n" + + "- name: app\n" + + " memory: 512M\n" + + "- name: anotherApp\n" + + " memory: 512M\n" + + "- name: someApp\n" + + " memory: 512M\n", + "app", "anotherApp", "someApp"); + } + + @Test + public void testNoAppName() throws Exception { + testAppNames( + "applications:\n" + + " memory: 512M\n"); + } + + @Test + public void testNoAppDueToSyntaxErrorNames() throws Exception { + testAppNames( + "applications:\n" + + "- name: app\n" + + " memory 512M\n" + + "- name: anotherApp\n" + + " memory: 512M\n" + + "- name: someApp\n" + + " memory: 512M\n"); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/CFRouteTests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/CFRouteTests.java new file mode 100644 index 000000000..f986ddf1f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/CFRouteTests.java @@ -0,0 +1,590 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test.yaml; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFRoute; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFRouteBuilder; + +public class CFRouteTests { + + public static final List SPRING_CLOUD_DOMAINS = Arrays.asList("springsource.org", "spring.io", + "myowndomain.spring.io", "tcp.spring.io", "spring.framework"); + + + @Test + public void test_domain_host() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io", route.getRoute()); + } + + @Test + public void test_domain_only() throws Exception { + CFRoute route = CFRoute.builder().from("spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("spring.io", route.getRoute()); + } + + @Test + public void test_longer_domain_match() throws Exception { + CFRoute route = CFRoute.builder().from("myowndomain.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("myowndomain.spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myowndomain.spring.io", route.getRoute()); + } + + @Test + public void test_longer_domain_nonexisting() throws Exception { + // For domains that do not exist, the first segment is assumed to be the "host" + CFRoute route = CFRoute.builder().from("app.doesnotexist.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("doesnotexist.io", route.getDomain()); + Assert.assertEquals("app",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("app.doesnotexist.io", route.getRoute()); + } + + @Test + public void test_longer_domain_nonexisting_path() throws Exception { + CFRoute route = CFRoute.builder().from("app.doesnotexist.io/withpath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("doesnotexist.io", route.getDomain()); + Assert.assertEquals("app",route.getHost()); + Assert.assertEquals("/withpath",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("app.doesnotexist.io/withpath", route.getRoute()); + } + + @Test + public void test_longer_domain_nonexisting_path_port() throws Exception { + CFRoute route = CFRoute.builder().from("app.doesnotexist.io:60100/withpath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("doesnotexist.io", route.getDomain()); + Assert.assertEquals("app",route.getHost()); + Assert.assertEquals("/withpath",route.getPath()); + Assert.assertEquals(60100, route.getPort()); + Assert.assertEquals("app.doesnotexist.io:60100/withpath", route.getRoute()); + } + + @Test + public void test_longer_domain_match_2() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.myowndomain.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("myowndomain.spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.myowndomain.spring.io", route.getRoute()); + } + + @Test + public void test_domain_host_path() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io/appPath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals("/appPath", route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io/appPath", route.getRoute()); + } + + @Test + public void test_domain_host_path_2() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io/appPath/additionalSegment", SPRING_CLOUD_DOMAINS) + .build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals("/appPath/additionalSegment", route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io/appPath/additionalSegment", route.getRoute()); + } + + @Test + public void test_tcp_port() throws Exception { + CFRoute route = CFRoute.builder().from("tcp.spring.io:9000", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("tcp.spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(9000, route.getPort()); + Assert.assertEquals("tcp.spring.io:9000", route.getRoute()); + } + + @Test + public void test_host_path() throws Exception { + CFRoute route = CFRoute.builder().from("justhost/path", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("justhost",route.getHost()); + Assert.assertEquals("/path",route.getPath()); + Assert.assertEquals("justhost/path",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + } + + @Test + public void test_routes() throws Exception { + // A CFRoute does not validate route values. It can create any CF route even with wrong domains + // ports, hosts.. This tests that the route builder is parsing an invalid route into different + // components that some other external mechanism (like the CF Java client) can the use to validate + + CFRoute route = CFRoute.builder().from("", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE,route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from(null, SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE,route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from(".", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(".",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("justhost", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("justhost",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("justhost",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("justhost.", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("justhost",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("justhost.",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from(".justdomain", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("justdomain",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(".justdomain",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("..justdomain", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals(".justdomain",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("..justdomain",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + + route = CFRoute.builder().from("/justpath/morepath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/justpath/morepath",route.getPath()); + Assert.assertEquals("/justpath/morepath",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("/", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/",route.getPath()); + Assert.assertEquals("/",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + } + + @Test + public void test_incorrect_ports() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io:notAn1nt3g3r", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("myapp.spring.io:notAn1nt3g3r",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + // Test parsing around the first encountered ':' + route = CFRoute.builder().from("https://myapp.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("https://myapp.spring.io",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("tcp.spring.io:8000:9000", SPRING_CLOUD_DOMAINS).build(); + // Only one ':' is allowed. it should not be able to parse a port if more than ':' is encountered + Assert.assertEquals("tcp.spring.io:8000:9000",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + + route = CFRoute.builder().from("myapp.spring.io:8000/", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/",route.getPath()); + Assert.assertEquals("myapp.spring.io:8000/",route.getRoute()); + Assert.assertEquals(8000, route.getPort()); + } + + @Test + public void test_incorrect_paths() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io//path", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("//path",route.getPath()); + Assert.assertEquals("myapp.spring.io//path",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("myapp.spring.io/", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/",route.getPath()); + Assert.assertEquals("myapp.spring.io/",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + } + + @Test + public void parse_null_domain() throws Exception { + String domain = CFRouteBuilder.findDomain("", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(null, SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(".", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(".cfapps", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("cfapps.", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("...", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("..cfapps..", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(".cfapps..", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("..cfapps.", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + } + + @Test + public void parse_valid_domain() throws Exception { + + // These exist + String domain = CFRouteBuilder.findDomain("spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain(".spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain("..spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain("myapp.spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain("myowndomain.spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("myowndomain.spring.io", domain); + + domain = CFRouteBuilder.findDomain("myapp.myowndomain.spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("myowndomain.spring.io", domain); + } + + @Test + public void parse_invalid_domain() throws Exception { + + // These variations of existing domains don't exist + String domain = CFRouteBuilder.findDomain("spring.io.", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("spring.cfapps.io", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("spring.io.cfapps", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("unknown", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("unknown.domain.io", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + } + + @Test + public void bug_142279275_parse_hostAndPathSameName() throws Exception { + + // Fixes Pivotal Tracker item 142279275 + CFRoute route = CFRoute.builder().from("hello-user.myowndomain.spring.io/hello", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("hello-user", route.getHost()); + Assert.assertEquals("myowndomain.spring.io", route.getDomain()); + Assert.assertEquals("/hello", route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("hello-user.myowndomain.spring.io/hello", route.getRoute()); + } + + @Test + public void build_route_value_empty() throws Exception { + + String val = CFRouteBuilder.buildRouteVal(null, null, null, CFRoute.NO_PORT); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, val); + + val = CFRouteBuilder.buildRouteVal("", "", "", CFRoute.NO_PORT); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, val); + + } + + @Test + public void build_route_value() throws Exception { + + String val = CFRouteBuilder.buildRouteVal("appHost", null, null, CFRoute.NO_PORT); + Assert.assertEquals("appHost", val); + + val = CFRouteBuilder.buildRouteVal(null, "cfapps.io", "", CFRoute.NO_PORT); + Assert.assertEquals("cfapps.io", val); + + val = CFRouteBuilder.buildRouteVal("appHost", "cfapps.io", "", CFRoute.NO_PORT); + Assert.assertEquals("appHost.cfapps.io", val); + + val = CFRouteBuilder.buildRouteVal(null, null, "/path/to/app", CFRoute.NO_PORT); + Assert.assertEquals("/path/to/app", val); + + val = CFRouteBuilder.buildRouteVal(null, null, "/path/to/app", 8000); + Assert.assertEquals(":8000/path/to/app", val); + + val = CFRouteBuilder.buildRouteVal(null, null, null, 60101); + Assert.assertEquals(":60101", val); + + val = CFRouteBuilder.buildRouteVal("appHost", "cfapps.io", "/path/to/app", CFRoute.NO_PORT); + Assert.assertEquals("appHost.cfapps.io/path/to/app", val); + + val = CFRouteBuilder.buildRouteVal("appHost", "cfapps.io", "/path/to/app", 60101); + Assert.assertEquals("appHost.cfapps.io:60101/path/to/app", val); + } + + @Test + public void test_build_route_from_domain() throws Exception { + CFRoute route = CFRoute.builder().domain("spring.io").build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("spring.io", route.getRoute()); + } + + @Test + public void test_build_route_from_nonexisting_domain() throws Exception { + CFRoute route = CFRoute.builder().domain("not.exist.io").build(); + Assert.assertEquals("not.exist.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("not.exist.io", route.getRoute()); + } + + @Test + public void test_build_route_from_null_domain() throws Exception { + CFRoute route = CFRoute.builder().domain(null).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_empty_domain() throws Exception { + CFRoute route = CFRoute.builder().domain("").build(); + Assert.assertEquals("",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE,route.getRoute()); + } + + @Test + public void test_build_route_from_host() throws Exception { + CFRoute route = CFRoute.builder().host("myapp").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp", route.getRoute()); + } + + @Test + public void test_build_route_from_null_host() throws Exception { + CFRoute route = CFRoute.builder().host(null).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getPath()); + Assert.assertNull(route.getHost()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_empty_host() throws Exception { + CFRoute route = CFRoute.builder().host("").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("",route.getHost()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_domain_host() throws Exception { + CFRoute route = CFRoute.builder().domain("spring.io").host("myapp").build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io", route.getRoute()); + } + + @Test + public void test_build_route_from_path() throws Exception { + CFRoute route = CFRoute.builder().path("/path").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/path",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("/path", route.getRoute()); + } + + @Test + public void test_build_route_from_path_2() throws Exception { + CFRoute route = CFRoute.builder().path("/path/additional").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/path/additional",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("/path/additional", route.getRoute()); + } + + @Test + public void test_build_route_from_path_3() throws Exception { + CFRoute route = CFRoute.builder().path("/path/").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/path/",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("/path/", route.getRoute()); + } + + @Test + public void test_build_route_from_domain_path() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").domain("spring.io").build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("spring.io/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_host_path() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").host("myapp").build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_host_path_samename() throws Exception { + CFRoute route = CFRoute.builder().path("/myapp").host("myapp").build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/myapp",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp/myapp", route.getRoute()); + } + + @Test + public void test_build_route_from_host_path_port() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").host("myapp").port(8000).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("myapp:8000/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_domain_path_port() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").domain("spring.io").port(8000).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("spring.io:8000/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_port() throws Exception { + CFRoute route = CFRoute.builder().port(8000).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals(":8000", route.getRoute()); + } + + + @Test + public void test_build_route_from_no_port() throws Exception { + CFRoute route = CFRoute.builder().port(CFRoute.NO_PORT).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_no_port_2() throws Exception { + CFRoute route = CFRoute.builder().port(-1).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_tcp_port_building() throws Exception { + CFRoute route = CFRoute.builder().domain("tcp.spring.io").port(8000).build(); + Assert.assertEquals("tcp.spring.io",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("tcp.spring.io:8000", route.getRoute()); + } + + + @Test + public void test_complete() throws Exception { + CFRoute route = CFRoute.builder().domain("spring.io").host("myapp").path("/mypath/additional").port(8000).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals("/mypath/additional", route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("myapp.spring.io:8000/mypath/additional", route.getRoute()); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/DeploymentProperties2YamlTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/DeploymentProperties2YamlTest.java new file mode 100644 index 000000000..82c8fed61 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/DeploymentProperties2YamlTest.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * Copyright (c) 2016, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test.yaml; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.FileInputStream; +import java.util.Arrays; +import java.util.Map; + +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.ApplicationManifestHandler; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.DumperOptions.FlowStyle; +import org.yaml.snakeyaml.DumperOptions.LineBreak; +import org.yaml.snakeyaml.Yaml; + +/** + * Tests for generating YAML files from {@link CloudApplicationDeploymentProperties} + * + * @author Alex Boyko + * + */ +public class DeploymentProperties2YamlTest { + + private static void testDeploymentProperties(CloudApplicationDeploymentProperties props, String expectedYamlFilePath) throws Exception { + Map map = ApplicationManifestHandler.toYaml(props, ManifestCompareMergeTests.createCloudData()); + + DumperOptions options = new DumperOptions(); + options.setExplicitStart(true); + options.setCanonical(false); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(FlowStyle.BLOCK); + options.setLineBreak(LineBreak.getPlatformLineBreak()); + + String generatedManifest = new Yaml(options).dump(map); + + File yamlFile = ManifestCompareMergeTests.getTestFile(expectedYamlFilePath); + FileInputStream inputStream = null; + try { + inputStream = new FileInputStream(yamlFile); + assertEquals(IOUtil.toString(inputStream).trim(), generatedManifest.trim()); + } finally { + if (inputStream != null) { + inputStream.close(); + } + } + } + + @Test + public void test_no_route_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(512); + props.setUris(null); + testDeploymentProperties(props, "manifest-generate-data/no-route-1.yml"); + } + + @Test + public void test_uri_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(512); + props.setUris(Arrays.asList("app.springsource.org")); + testDeploymentProperties(props, "manifest-generate-data/uri-1.yml"); + } + + @Test + public void test_uri_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(512); + props.setUris(Arrays.asList("app-1.springsource.org", "app-2.spring.io")); + testDeploymentProperties(props, "manifest-generate-data/uri-2.yml"); + } + + @Test + public void test_uri_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(512); + props.setUris(Arrays.asList("app-1.springsource.org")); + testDeploymentProperties(props, "manifest-generate-data/uri-3.yml"); + } + + @Test + public void test_no_hostname_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(512); + props.setUris(Arrays.asList("springsource.org")); + testDeploymentProperties(props, "manifest-generate-data/no-hostname-1.yml"); + } + + @Test + public void test_no_hostname_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(512); + props.setUris(Arrays.asList("springsource.org", "spring.io")); + testDeploymentProperties(props, "manifest-generate-data/no-hostname-2.yml"); + } + + @Test + public void test_command_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(512); + props.setCommand("my-command"); + props.setUris(Arrays.asList("app.springsource.org")); + testDeploymentProperties(props, "manifest-generate-data/command-1.yml"); + } + + @Test + public void test_stack_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(512); + props.setStack("my-stack"); + props.setUris(Arrays.asList("app.springsource.org")); + testDeploymentProperties(props, "manifest-generate-data/stack-1.yml"); + } + + + @Test + public void test_health_check_http() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(512); + props.setHealthCheckType("http"); + props.setHealthCheckHttpEndpoint("/testhealth"); + props.setUris(Arrays.asList("app.springsource.org")); + + testDeploymentProperties(props, "manifest-generate-data/health-check-http.yml"); + } + + + @Test + public void test_health_check_process() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(512); + props.setHealthCheckType("process"); + props.setUris(Arrays.asList("app.springsource.org")); + + testDeploymentProperties(props, "manifest-generate-data/health-check-process.yml"); + } + + @Test + public void test_health_check_port() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(512); + props.setHealthCheckType("port"); + props.setUris(Arrays.asList("app.springsource.org")); + + testDeploymentProperties(props, "manifest-generate-data/health-check-port.yml"); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/ManifestCompareMergeTests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/ManifestCompareMergeTests.java new file mode 100644 index 000000000..7fc910db4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/ManifestCompareMergeTests.java @@ -0,0 +1,1162 @@ +/******************************************************************************* + * Copyright (c) 2016, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test.yaml; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.text.Document; +import org.eclipse.text.edits.TextEdit; +import org.junit.Assert; +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFCloudDomain; +import org.springframework.ide.eclipse.boot.dash.cf.client.CFStack; +import org.springframework.ide.eclipse.boot.dash.cf.client.v2.CFCloudDomainData; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.ApplicationManifestHandler; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudData; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.YamlGraphDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.test.AllBootDashTests; +import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil; + +import com.google.common.collect.ImmutableList; + +/** + * Manifest YAML file and Deployment properties compare and merge tests. + * + * @author Alex Boyko + * + */ +public class ManifestCompareMergeTests { + + public static final String DEFAULT_BUILDPACK = "java_buildpack_offline"; + + public static final List SPRING_CLOUD_DOMAINS = Arrays.asList( + new CFCloudDomainData("springsource.org"), + new CFCloudDomainData("spring.io"), + new CFCloudDomainData("spring.framework")); + + public static final List SPRING_CLOUD_STACKS = Arrays.asList(new CFStack[] { new CFStack() { + @Override + public String getName() { + return "stack1"; + } + }}); + + public static CloudData createCloudData() { + return new CloudData(SPRING_CLOUD_DOMAINS, DEFAULT_BUILDPACK, SPRING_CLOUD_STACKS); + } + + private static void performMergeTest(File manifest, DeploymentProperties props, File expected) throws Exception { + String yamlContents = IOUtil.toString(new FileInputStream(manifest)); + String expectText = expected == null ? null : IOUtil.toString(new FileInputStream(expected)); + //Note: You don't need to close the FileInputStreams because IOUtil does that already. + performMergeTest(props, yamlContents, expectText); + } + + private static void performMergeTest(DeploymentProperties props, String manifest, String expectText) throws Exception { + YamlGraphDeploymentProperties yamlGraphProps = new YamlGraphDeploymentProperties(manifest, props.getAppName(), createCloudData()); + TextEdit edit = yamlGraphProps.getDifferences(props); + if (expectText == null) { + assertEquals(null, edit); + } else { + Document doc = new Document(manifest); + if (edit!=null) { + edit.apply(doc); + } + assertEquals(expectText.trim(), doc.get().trim()); + } + } + + public static File getTestFile(String path) throws IOException { + Bundle bundle = Platform.getBundle(AllBootDashTests.PLUGIN_ID); + File bundleFile = FileLocator.getBundleFile(bundle); + Assert.assertNotNull(bundleFile); + Assert.assertTrue("The bundle "+bundle.getBundleId()+" must be unpacked to allow using the embedded test resources", bundleFile.isDirectory()); + return new File(bundleFile, path); + } + + @Test + public void anchorReferenceAndExtend() throws Exception { + String manifest = + "defaults: &defaults\n" + + " services:\n" + + " - my-rabbit\n" + + " memory: 1G\n" + + " instances: 3\n" + + "applications:\n" + + "- name: chatter-web-ui\n" + + " <<: *defaults\n" + + " buildpack: java_buildpack"; + + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("chatter-web-ui"); + props.setServices(ImmutableList.of("my-rabbit")); + props.setMemory(1024); + props.setInstances(3); + props.setMemory(1024); + props.setBuildpack("java_buildpack"); + props.setUris(ImmutableList.of("chatter-web-ui.springsource.org")); + performMergeTest(props, manifest, manifest); + + props.setInstances(2); + performMergeTest(props, manifest, + "defaults: &defaults\n" + + " services:\n" + + " - my-rabbit\n" + + " memory: 1G\n" + + " instances: 2\n" + + "applications:\n" + + "- name: chatter-web-ui\n" + + " <<: *defaults\n" + + " buildpack: java_buildpack" + ); + } + + @Test + public void test_health_check_port() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setHealthCheckType("port"); + + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + , // ==> + null + ); + + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: port\n" + , // ==> + null + ); + + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: none\n" + , // ==> + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + ); + } + + @Test + public void test_health_check_none() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setHealthCheckType("none"); + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + , // ==> + "applications:\n" + + "- name: app\n" + + " health-check-type: none\n" + + " no-route: true\n" + ); + + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: port\n" + , // ==> + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: none\n" + ); + + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: none\n" + , // ==> + null + ); + } + + @Test + public void test_health_check_process() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setHealthCheckType("process"); + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + , // ==> + "applications:\n" + + "- name: app\n" + + " health-check-type: process\n" + + " no-route: true\n" + ); + + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: port\n" + , // ==> + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: process\n" + ); + + // Test switch from http and http endpoint to process + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: http\n" + + " health-check-http-endpoint: /testhealth\n" + , // ==> + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: process\n" + ); + + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: process\n" + , // ==> + null + ); + } + + @Test + public void test_health_check_http_endpoint() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + // test both "http" type and "http endpoint", as both are used together + props.setHealthCheckType("http"); + props.setHealthCheckHttpEndpoint("/testhealth"); + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + , // ==> + "applications:\n" + + "- name: app\n" + + " health-check-type: http\n" + + " health-check-http-endpoint: /testhealth\n" + + " no-route: true\n" + ); + + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: port\n" + , // ==> + "applications:\n" + + "- name: app\n" + + " health-check-http-endpoint: /testhealth\n" + + " no-route: true\n" + + " health-check-type: http\n" + ); + + // Test that old endpoint is replaced with new one + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: http\n" + + " health-check-http-endpoint: /oldtesthealth\n" + , // ==> + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: http\n" + + " health-check-http-endpoint: /testhealth\n" + ); + + performMergeTest(props, + "applications:\n" + + "- name: app\n" + + " no-route: true\n" + + " health-check-type: http\n" + + " health-check-http-endpoint: /testhealth\n" + , // ==> + null + ); + } + + + @Test + public void test_memory_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/memory-1.yml"), props, getTestFile("mergeTestsData/memory-1-expected.yml")); + } + + @Test + public void test_memory_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/memory-2.yml"), props, getTestFile("mergeTestsData/memory-2-expected.yml")); + } + + @Test + public void test_memory_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/memory-3.yml"), props, getTestFile("mergeTestsData/memory-3-expected.yml")); + } + + @Test + public void test_memory_4() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/memory-4.yml"), props, null); + } + + @Test + public void test_memory_5() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/memory-5.yml"), props, null); + } + + @Test + public void test_memory_6() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(3000); + performMergeTest(getTestFile("mergeTestsData/memory-5.yml"), props, getTestFile("mergeTestsData/memory-6-expected.yml")); + } + + @Test + public void test_appNameNoMatch_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app1"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/appNameNoMatch-1.yml"), props, getTestFile("mergeTestsData/appNameNoMatch-1-expected.yml")); + } + + @Test + public void test_appNameNoMatch_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app1"); + props.setUris(Collections.singletonList("test-app-1.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/appNameNoMatch-2.yml"), props, getTestFile("mergeTestsData/appNameNoMatch-2-expected.yml")); + } + + @Test + public void test_appNameNoMatch_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app1"); + props.setUris(Collections.singletonList("test-app-1.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/appNameNoMatch-3.yml"), props, getTestFile("mergeTestsData/appNameNoMatch-3-expected.yml")); + } + + @Test + public void test_appNameNoMatch_4() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app1"); + props.setUris(Collections.singletonList("app-1.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/appNameNoMatch-4.yml"), props, getTestFile("mergeTestsData/appNameNoMatch-4-expected.yml")); + } + + @Test + public void test_noAppsNode_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/noAppsNode-1.yml"), props, getTestFile("mergeTestsData/noAppsNode-1-expected.yml")); + } + + @Test + public void test_noAppsNode_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app1"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/noAppsNode-2.yml"), props, getTestFile("mergeTestsData/noAppsNode-2-expected.yml")); + } + + @Test + public void test_noManifest_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/noManifest-1.yml"), props, getTestFile("mergeTestsData/noManifest-1-expected.yml")); + } + + @Test + public void test_noManifest_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/noManifest-2.yml"), props, getTestFile("mergeTestsData/noManifest-2-expected.yml")); + } + + @Test + public void test_map_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + Map env = new LinkedHashMap<>(); + env.put("KEY1", "value1"); + env.put("KEY2", "value2"); + env.put("KEY3", "value3"); + props.setEnvironmentVariables(env); + performMergeTest(getTestFile("mergeTestsData/map-1.yml"), props, getTestFile("mergeTestsData/map-1-expected.yml")); + } + + @Test + public void test_map_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + Map env = new LinkedHashMap<>(); + env.put("KEY1", "value1"); + env.put("KEY2", "value2"); + env.put("KEY3", "value3"); + props.setEnvironmentVariables(env); + performMergeTest(getTestFile("mergeTestsData/map-2.yml"), props, getTestFile("mergeTestsData/map-2-expected.yml")); + } + + @Test + public void test_map_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/map-3.yml"), props, getTestFile("mergeTestsData/map-3-expected.yml")); + } + + @Test + public void test_map_4() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/map-4.yml"), props, getTestFile("mergeTestsData/map-4-expected.yml")); + } + + @Test + public void test_map_5() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + Map env = new LinkedHashMap<>(); + env.put("KEY1", "value1"); + env.put("KEY2", "value2"); + env.put("KEY3", "value3"); + env.put("KEY4", "value4"); + props.setEnvironmentVariables(env); + performMergeTest(getTestFile("mergeTestsData/map-5.yml"), props, getTestFile("mergeTestsData/map-5-expected.yml")); + } + + @Test + public void test_map_6() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + Map env = new LinkedHashMap<>(); + env.put("KEY1", "value1"); + env.put("KEY2", "value2"); + env.put("KEY3", "value3"); + env.put("KEY4", "value4"); + props.setEnvironmentVariables(env); + performMergeTest(getTestFile("mergeTestsData/map-6.yml"), props, getTestFile("mergeTestsData/map-6-expected.yml")); + } + + @Test + public void test_map_7() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + Map env = new LinkedHashMap<>(); + env.put("KEY1", "value1"); + env.put("KEY2", "value2"); + env.put("KEY3", "value3"); + env.put("KEY4", "value4"); + props.setEnvironmentVariables(env); + performMergeTest(getTestFile("mergeTestsData/map-7.yml"), props, getTestFile("mergeTestsData/map-7-expected.yml")); + } + + @Test + public void test_map_8() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/map-8.yml"), props, getTestFile("mergeTestsData/map-8-expected.yml")); + } + + @Test + public void test_map_9() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + Map env = new LinkedHashMap<>(); + env.put("KEY1", "value1"); + env.put("KEY2", "value2"); + env.put("KEY3", "value3"); + env.put("KEY4", "value4"); + props.setEnvironmentVariables(env); + performMergeTest(getTestFile("mergeTestsData/map-9.yml"), props, getTestFile("mergeTestsData/map-9-expected.yml")); + } + + @Test + public void test_map_10() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + Map env = new LinkedHashMap<>(); + env.put("KEY1", "value1"); + env.put("KEY2", "value2"); + env.put("KEY3", "value3"); + env.put("KEY4", "value4"); + props.setEnvironmentVariables(env); + performMergeTest(getTestFile("mergeTestsData/map-10.yml"), props, getTestFile("mergeTestsData/map-10-expected.yml")); + } + + @Test + public void test_map_11() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + Map env = new LinkedHashMap<>(); + env.put("KEY1", "value1"); + props.setEnvironmentVariables(env); + performMergeTest(getTestFile("mergeTestsData/map-11.yml"), props, getTestFile("mergeTestsData/map-11-expected.yml")); + } + + @Test + public void test_health_check_http() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setHealthCheckType("http"); + props.setHealthCheckHttpEndpoint("/testhealth"); + props.setUris(Collections.singletonList("app-1.springsource.org")); + + performMergeTest(getTestFile("mergeTestsData/health-check-http.yml"), props, getTestFile("mergeTestsData/health-check-http-expected.yml")); + } + + @Test + public void test_health_check_process_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("app-1.springsource.org")); + props.setMemory(2048); + props.setHealthCheckType("process"); + performMergeTest(getTestFile("mergeTestsData/health-check-process.yml"), props, getTestFile("mergeTestsData/health-check-process-expected.yml")); + } + + @Test + public void test_health_check_port_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("app-1.springsource.org")); + props.setMemory(2048); + // NOTE: port is a "default" value, so when setting port, the resulting manifest will have no health check type + // as no health check type is equivalent to having "port" type + props.setHealthCheckType("port"); + performMergeTest(getTestFile("mergeTestsData/health-check-port.yml"), props, getTestFile("mergeTestsData/health-check-port-expected.yml")); + } + + @Test + public void test_instances_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/instances-1.yml"), props, getTestFile("mergeTestsData/instances-1-expected.yml")); + } + + @Test + public void test_instances_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/instances-2.yml"), props, getTestFile("mergeTestsData/instances-2-expected.yml")); + } + + @Test + public void test_instances_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/instances-3.yml"), props, getTestFile("mergeTestsData/instances-3-expected.yml")); + } + + @Test + public void test_instances_4() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/instances-4.yml"), props, null); + } + + @Test + public void test_instances_5() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/instances-5.yml"), props, getTestFile("mergeTestsData/instances-5-expected.yml")); + } + + @Test + public void test_root_comment_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(512); + performMergeTest(getTestFile("mergeTestsData/root-comment-1.yml"), props, getTestFile("mergeTestsData/root-comment-1-expected.yml")); + } + + @Test + public void test_root_comment_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(512); + performMergeTest(getTestFile("mergeTestsData/root-comment-2.yml"), props, getTestFile("mergeTestsData/root-comment-2-expected.yml")); + } + + @Test + public void test_root_comment_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(512); + props.setInstances(2); + performMergeTest(getTestFile("mergeTestsData/root-comment-3.yml"), props, getTestFile("mergeTestsData/root-comment-3-expected.yml")); + } + + @Test + public void test_root_comment_4() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.singletonList("test-app.springsource.org")); + props.setMemory(512); + performMergeTest(getTestFile("mergeTestsData/root-comment-4.yml"), props, getTestFile("mergeTestsData/root-comment-4-expected.yml")); + } + + @Test + public void test_no_route_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.emptyList()); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/no-route-1.yml"), props, getTestFile("mergeTestsData/no-route-1-expected.yml")); + } + + @Test + public void test_no_route_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.emptyList()); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/no-route-2.yml"), props, getTestFile("mergeTestsData/no-route-2-expected.yml")); + } + + @Test + public void test_no_route_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.emptyList()); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/no-route-3.yml"), props, null); + } + + @Test + public void test_no_hostname_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("my-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/no-hostname-1.yml"), props, getTestFile("mergeTestsData/no-hostname-1-expected.yml")); + } + + @Test + public void test_no_hostname_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/no-hostname-2.yml"), props, getTestFile("mergeTestsData/no-hostname-2-expected.yml")); + } + + @Test + public void test_no_hostname_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/no-hostname-3.yml"), props, null); + } + + @Test + public void test_random_route_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/random-route-1.yml"), props, null); + } + + @Test + public void test_random_route_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("my-app1.springsource.org", "my-app2.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/random-route-2.yml"), props, getTestFile("mergeTestsData/random-route-2-expected.yml")); + } + + @Test + public void test_random_route_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("app2.springsource.org")); + props.setMemory(1024); + props.setBuildpack("java_buildpack_offline"); + performMergeTest(getTestFile("mergeTestsData/random-route-3.yml"), props, getTestFile("mergeTestsData/random-route-3-expected.yml")); + } + + @Test + public void test_hosts_domains_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("my-app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/hosts-domains-1.yml"), props, getTestFile("mergeTestsData/hosts-domains-1-expected.yml")); + } + + @Test + public void test_hosts_domains_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("app1.springsource.org", "app2.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/hosts-domains-2.yml"), props, getTestFile("mergeTestsData/hosts-domains-2-expected.yml")); + } + + @Test + public void test_hosts_domains_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("app1.springsource.org", "app2.springsource.org", "app1.spring.io", "app2.spring.io")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/hosts-domains-3.yml"), props, null); + } + + @Test + public void test_hosts_domains_4() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("app1.springsource.org", "app2.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/hosts-domains-4.yml"), props, getTestFile("mergeTestsData/hosts-domains-4-expected.yml")); + } + + @Test + public void test_hosts_domains_5() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("app.springsource.org")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/hosts-domains-5.yml"), props, getTestFile("mergeTestsData/hosts-domains-5-expected.yml")); + } + + @Test + public void test_hosts_domains_6() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("spring.framework")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/hosts-domains-6.yml"), props, getTestFile("mergeTestsData/hosts-domains-6-expected.yml")); + } + + @Test + public void test_hosts_domains_7() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("app1.springsource.org", "app2.springsource.org", "app3.springsource.org", + "app1.spring.io", "app2.spring.io", "app3.spring.io", "app1.spring.framework", "app2.spring.framework", + "app3.spring.framework")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/hosts-domains-7.yml"), props, null); + } + + @Test + public void test_hosts_domains_8() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("app1.spring.io", "app2.spring.io")); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/hosts-domains-8.yml"), props, getTestFile("mergeTestsData/hosts-domains-8-expected.yml")); + } + + @Test + public void test_root_node_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(3); + props.setUris(Arrays.asList("app.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/root-node-1.yml"), props, getTestFile("mergeTestsData/root-node-1-expected.yml")); + } + + @Test + public void test_root_node_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/root-node-2.yml"), props, getTestFile("mergeTestsData/root-node-2-expected.yml")); + } + + @Test + public void test_root_list_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app.springsource.org")); + props.setServices(Arrays.asList("s1", "s2", "s3")); + performMergeTest(getTestFile("mergeTestsData/root-list-1.yml"), props, getTestFile("mergeTestsData/root-list-1-expected.yml")); + } + + @Test + public void test_root_list_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app.springsource.org")); + props.setServices(Arrays.asList("s1", "s2", "s3", "s4", "s5")); + performMergeTest(getTestFile("mergeTestsData/root-list-2.yml"), props, getTestFile("mergeTestsData/root-list-2-expected.yml")); + } + + @Test + public void test_root_list_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app.springsource.org")); + props.setServices(Arrays.asList("s1", "s3", "s4", "s5")); + performMergeTest(getTestFile("mergeTestsData/root-list-3.yml"), props, getTestFile("mergeTestsData/root-list-3-expected.yml")); + } + + @Test + public void test_root_map_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app.springsource.org")); + Map env = new HashMap<>(); + env.put("k1", "v1"); + env.put("k2", "v2"); + env.put("k3", "v3"); + props.setEnvironmentVariables(env); + performMergeTest(getTestFile("mergeTestsData/root-map-1.yml"), props, getTestFile("mergeTestsData/root-map-1-expected.yml")); + } + + @Test + public void test_root_map_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app.springsource.org")); + Map env = new HashMap<>(); + env.put("k1", "v1"); + env.put("k2", "v2-alt"); + props.setEnvironmentVariables(env); + performMergeTest(getTestFile("mergeTestsData/root-map-2.yml"), props, getTestFile("mergeTestsData/root-map-2-expected.yml")); + } + + @Test + public void test_root_map_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/root-map-3.yml"), props, getTestFile("mergeTestsData/root-map-3-expected.yml")); + } + + @Test + public void test_root_map_4() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app.springsource.org")); + Map env = new HashMap<>(); + env.put("k2", "v2"); + props.setEnvironmentVariables(env); + performMergeTest(getTestFile("mergeTestsData/root-map-4.yml"), props, getTestFile("mergeTestsData/root-map-4-expected.yml")); + } + + @Test + public void test_root_noroute_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/root-noroute-1.yml"), props, getTestFile("mergeTestsData/root-noroute-1-expected.yml")); + } + + @Test + public void test_root_noroute_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + performMergeTest(getTestFile("mergeTestsData/root-noroute-2.yml"), props, getTestFile("mergeTestsData/root-noroute-2-expected.yml")); + } + + @Test + public void test_root_randomroute_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app-1.springsource.org", "app-2.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/root-randomroute-1.yml"), props, getTestFile("mergeTestsData/root-randomroute-1-expected.yml")); + } + + @Test + public void test_root_randomroute_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/root-randomroute-2.yml"), props, null); + } + + @Test + public void test_root_nohost_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("my-app.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/root-nohost-1.yml"), props, getTestFile("mergeTestsData/root-nohost-1-expected.yml")); + } + + @Test + public void test_root_nohost_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("springsource.org", "spring.io")); + performMergeTest(getTestFile("mergeTestsData/root-nohost-2.yml"), props, getTestFile("mergeTestsData/root-nohost-2-expected.yml")); + } + + @Test + public void test_root_nohost_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("springsource.org")); + performMergeTest(getTestFile("mergeTestsData/root-nohost-3.yml"), props, null); + } + + @Test + public void test_root_hosts_domains_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app1.springsource.org", "app2.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/root-hosts-domains-1.yml"), props, getTestFile("mergeTestsData/root-hosts-domains-1-expected.yml")); + } + + @Test + public void test_root_hosts_domains_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("my-app.springsource.org", "test-app1.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/root-hosts-domains-2.yml"), props, getTestFile("mergeTestsData/root-hosts-domains-2-expected.yml")); + } + + @Test + public void test_root_hosts_domains_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("test-app-1.springsource.org", "test-app-3.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/root-hosts-domains-3.yml"), props, getTestFile("mergeTestsData/root-hosts-domains-3-expected.yml")); + } + + @Test + public void test_routes_1() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app-1.springsource.org", "app-2.springsource.org", "app-3.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/routes-1.yml"), props, getTestFile("mergeTestsData/routes-1-expected.yml")); + } + + @Test + public void test_routes_2() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app-1.springsource.org", "app-2.springsource.org", "app-3.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/routes-2.yml"), props, getTestFile("mergeTestsData/routes-2-expected.yml")); + } + + @Test + public void test_routes_3() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app-1.springsource.org", "app-2.springsource.org", "app-3.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/routes-3.yml"), props, getTestFile("mergeTestsData/routes-3-expected.yml")); + } + + @Test + public void test_routes_4() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app-1.springsource.org", "app-2.springsource.org", "app-3.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/routes-4.yml"), props, getTestFile("mergeTestsData/routes-4-expected.yml")); + } + + @Test + public void test_routes_5() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app-1.springsource.org", "app-2.springsource.org", "app-3.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/routes-5.yml"), props, getTestFile("mergeTestsData/routes-5-expected.yml")); + } + + @Test + public void test_routes_6() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("demo-app"); + props.setMemory(1024); + props.setInstances(1); + props.setUris(Arrays.asList("route-2.springsource.org")); + performMergeTest(getTestFile("mergeTestsData/routes-6.yml"), props, getTestFile("mergeTestsData/routes-6-expected.yml")); + } + + @Test + public void test_routes_paths_ports() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setMemory(2048); + props.setInstances(1); + props.setUris(Arrays.asList("app-1.springsource.org", "app-2.springsource.org", "app-3.springsource.org", "app-4.springsource.org/myappPath/moresegments", "tcp.springsource.org:9003")); + performMergeTest(getTestFile("mergeTestsData/routes-paths-ports.yml"), props, getTestFile("mergeTestsData/routes-paths-ports-expected.yml")); + } + + @Test + public void test_no_route_5() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Collections.emptyList()); + props.setMemory(2048); + performMergeTest(getTestFile("mergeTestsData/no-route-5.yml"), props, getTestFile("mergeTestsData/no-route-5-expected.yml")); + } + + @Test + public void test_random_route_4() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("app-1.springsource.org", "app-2.springsource.org", "app-3.springsource.org")); + props.setMemory(512); + performMergeTest(getTestFile("mergeTestsData/random-route-4.yml"), props, null); + } + + @Test + public void test_random_route_5() throws Exception { + CloudApplicationDeploymentProperties props = new CloudApplicationDeploymentProperties(); + props.setAppName("app"); + props.setUris(Arrays.asList("app.springsource.org")); + props.setMemory(512); + performMergeTest(getTestFile("mergeTestsData/random-route-5.yml"), props, getTestFile("mergeTestsData/random-route-5-expected.yml")); + } +} + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/Yaml2DeploymentPropertiesTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/Yaml2DeploymentPropertiesTest.java new file mode 100644 index 000000000..5272376b4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/src/org/springframework/ide/eclipse/boot/dash/test/yaml/Yaml2DeploymentPropertiesTest.java @@ -0,0 +1,477 @@ +/******************************************************************************* + * Copyright (c) 2016, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.test.yaml; + +import static org.junit.Assert.assertEquals; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.junit.Test; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.ApplicationManifestHandler; +import org.springframework.ide.eclipse.boot.dash.cf.deployment.CloudApplicationDeploymentProperties; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeploymentProperties; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * Tests for parsing YAML deployment manifest into + * {@link CloudApplicationDeploymentProperties} + * + * @author Alex Boyko + * + */ +public class Yaml2DeploymentPropertiesTest { + + private static CloudApplicationDeploymentProperties readDeploymentPropertiesFile(final String filePath) throws Exception { + ApplicationManifestHandler handler = new ApplicationManifestHandler(null, ManifestCompareMergeTests.createCloudData()) { + @Override + protected InputStream getInputStream() throws Exception { + return new FileInputStream(ManifestCompareMergeTests.getTestFile(filePath)); + } + }; + return handler.load(new NullProgressMonitor()).get(0); + } + + @Test + public void test_health_check_process() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/health-check-process.yml"); + assertEquals("Health check types not equal", "process", props.getHealthCheckType()); + } + + @Test + public void test_health_check_port() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/health-check-port.yml"); + assertEquals("Health check types not equal", "port", props.getHealthCheckType()); + } + + @Test + public void test_health_check_http() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/health-check-http.yml"); + assertEquals("Health check types not equal", "http", props.getHealthCheckType()); + assertEquals("Health check http endpoints not equal", "/testhealth", props.getHealthCheckHttpEndpoint()); + } + + @Test + public void test_no_route_1() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/no-route-1.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_no_route_2() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/no-route-2.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_no_route_3() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/no-route-3.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_no_route_4() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/no-route-4.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_no_route_5() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/no-route-5.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_no_hostname_1() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/no-hostname-1.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("my-app.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_no_hostname_2() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/no-hostname-2.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("my-app.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_no_hostname_3() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/no-hostname-3.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("my-app.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_no_hostname_4() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/no-hostname-4.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_no_hostname_5() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/no-hostname-5.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_no_hostname_6() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/no-hostname-6.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("springsource.org", "spring.framework")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_random_route_1() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/random-route-1.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("my-app.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_random_route_2() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/random-route-2.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("my-app.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_random_route_3() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/random-route-3.yml"); + HashSet uris = new HashSet<>(props.getUris()); + assertEquals(1, uris.size()); + String uri = uris.iterator().next(); + String host = uri.substring(0, uri.indexOf('.')); + HashSet expected = new HashSet<>(Collections.singletonList("springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_random_route_4() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/random-route-4.yml"); + HashSet uris = new HashSet<>(props.getUris()); + assertEquals(1, uris.size()); + String uri = uris.iterator().next(); + String host = uri.substring(0, uri.indexOf('.')); + HashSet expected = new HashSet<>(Collections.singletonList("springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_random_route_5() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/random-route-5.yml"); + HashSet uris = new HashSet<>(props.getUris()); + Set expected = ImmutableSet.of("spring.io", "spring.framework"); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_domains_1() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/domains-1.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("my-app.spring.io")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_domains_2() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/domains-2.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("my-app.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_domains_3() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/domains-3.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("my-app.spring.framework")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_domains_4() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/domains-4.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("my-app.spring.io")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_domains_5() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/domains-5.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("my-app.spring.framework", "my-app.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_domains_6() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/domains-6.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Collections.singletonList("my-app.spring.what")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_domains_7() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/domains-7.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("my-app.springsource.org", "my-app.spring.framework", "my-app.spring.io")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_domains_8() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/domains-8.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("my-app.springsource.org", "my-app.spring.framework", "my-app.spring.io")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_domains_9() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/domains-9.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("my-app.springsource.org", "my-app.spring.framework", "my-app.spring.io")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_hosts_1() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/hosts-1.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("app.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_hosts_2() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/hosts-2.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("my-app.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_hosts_3() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/hosts-3.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("my-app.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_hosts_4() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/hosts-4.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("my-app.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_hosts_5() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/hosts-5.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("my-app-1.springsource.org", "my-app-2.springsource.org", "my-app-3.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_hosts_6() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/hosts-6.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("my-app-1.springsource.org", "my-app-2.springsource.org", + "my-app-3.springsource.org", "my-root-2.springsource.org", "my-root-3.springsource.org")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_uris_1() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/uris-1.yml"); + HashSet uris = new HashSet<>(props.getUris()); + HashSet expected = new HashSet<>(Arrays.asList("my-app-1.springsource.org", "my-app-2.springsource.org", + "my-app-3.springsource.org", "my-app-1.spring.io", "my-app-2.spring.io", "my-app-3.spring.io", + "my-app-1.spring.framework", "my-app-2.spring.framework", "my-app-3.spring.framework")); + assertEquals("Uris sets not equal", expected, uris); + } + + @Test + public void test_root_1() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/root-1.yml"); + assertEquals("app", props.getAppName()); + assertEquals(1024, props.getMemory()); + assertEquals(new HashSet<>(Arrays.asList("app.spring.io")), props.getUris()); + } + + @Test + public void test_command_1() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/command-1.yml"); + assertEquals("mycommand", props.getCommand()); + } + + @Test + public void test_stack_1() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/stack-1.yml"); + assertEquals("stack1", props.getStack()); + } + + @Test + public void test_memory_1() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/memory-1.yml"); + assertEquals("Uris sets not equal", DeploymentProperties.DEFAULT_MEMORY, props.getMemory()); + } + + @Test + public void test_memory_2() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/memory-2.yml"); + assertEquals("Uris sets not equal", 768, props.getMemory()); + } + + @Test + public void test_memory_3() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/memory-3.yml"); + assertEquals("Uris sets not equal", 768, props.getMemory()); + } + + @Test + public void test_memory_4() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/memory-4.yml"); + assertEquals("Uris sets not equal", 768, props.getMemory()); + } + + @Test + public void test_memory_5() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/memory-5.yml"); + assertEquals("Uris sets not equal", 1024, props.getMemory()); + } + + @Test + public void test_memory_6() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/memory-6.yml"); + assertEquals("Uris sets not equal", 1024, props.getMemory()); + } + + @Test(expected=CoreException.class) + public void test_memory_7() throws Exception { + readDeploymentPropertiesFile("manifest-parse-data/memory-7.yml"); + } + + @Test(expected=CoreException.class) + public void test_memory_8() throws Exception { + readDeploymentPropertiesFile("manifest-parse-data/memory-8.yml"); + } + + @Test + public void test_memory_9() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/memory-9.yml"); + assertEquals("Uris sets not equal", 3072, props.getMemory()); + } + + @Test + public void test_memory_10() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/memory-10.yml"); + assertEquals("Uris sets not equal", 4096, props.getMemory()); + } + + @Test + public void test_memory_11() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile("manifest-parse-data/memory-11.yml"); + assertEquals("Uris sets not equal", 1500, props.getMemory()); + } + + @Test + public void test_routes_no_route() throws Exception { + // Manifest has a route but if "no-route" is also present, test that + // list of URI is empty in the props + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile( + "manifest-generate-data/routes-no-route.yml"); + + assertEquals("Routes and URIs not equal", ImmutableList.of(), ImmutableList.copyOf(props.getUris())); + } + + @Test + public void test_routes_1() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile( + "manifest-parse-data/routes-1.yml"); + + assertEquals("Routes not equal", ImmutableSet.of("my-route.springsource.org"), ImmutableSet.copyOf(props.getUris())); + } + + @Test + public void test_routes_2() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile( + "manifest-parse-data/routes-2.yml"); + + assertEquals("Routes not equal", ImmutableSet.of("my-route-1.springsource.org", "my-route-2.springsource.org"), ImmutableSet.copyOf(props.getUris())); + } + + @Test + public void test_routes_3() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile( + "manifest-parse-data/routes-3.yml"); + + assertEquals("Routes not equal", ImmutableSet.of(), ImmutableSet.copyOf(props.getUris())); + } + + @Test + public void test_routes_4() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile( + "manifest-parse-data/routes-4.yml"); + + assertEquals("Routes not equal", ImmutableSet.of("my-route-1.springsource.org", "my-route-2.springsource.org"), ImmutableSet.copyOf(props.getUris())); + } + + @Test + public void test_routes_5() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile( + "manifest-parse-data/routes-5.yml"); + + assertEquals("Routes not equal", ImmutableSet.of("my-route-1.springsource.org", "my-route-2.springsource.org", + "my-root-route-1.springsource.org"), ImmutableSet.copyOf(props.getUris())); + } + + @Test + public void test_routes_6() throws Exception { + CloudApplicationDeploymentProperties props = readDeploymentPropertiesFile( + "manifest-parse-data/routes-6.yml"); + + assertEquals("Routes not equal", ImmutableSet.of("my-route-1.invaliddomain.org", "my-route-2.springsource.org"), ImmutableSet.copyOf(props.getUris())); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/.gitignore b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/.gitignore new file mode 100644 index 000000000..fa8ebf2b1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/.gitignore @@ -0,0 +1,5 @@ +.project +.classpath +.settings +.mvn +target \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/pom.xml new file mode 100644 index 000000000..302cdce0f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + com.example + boot12 + 0.0.1-SNAPSHOT + jar + + boot12 + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 1.2.8.RELEASE + + + + + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/src/main/java/com/example/Boot12Application.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/src/main/java/com/example/Boot12Application.java new file mode 100644 index 000000000..c9a9a9395 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/src/main/java/com/example/Boot12Application.java @@ -0,0 +1,12 @@ +package com.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Boot12Application { + + public static void main(String[] args) { + SpringApplication.run(Boot12Application.class, args); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/src/main/resources/application.properties b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/src/main/resources/application.properties new file mode 100644 index 000000000..e69de29bb diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/src/test/java/com/example/Boot12ApplicationTests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/src/test/java/com/example/Boot12ApplicationTests.java new file mode 100644 index 000000000..6a82fd077 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/boot12/src/test/java/com/example/Boot12ApplicationTests.java @@ -0,0 +1,18 @@ +package com.example; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = Boot12Application.class) +@WebAppConfiguration +public class Boot12ApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.classpath new file mode 100644 index 000000000..9fc2de7b0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.classpath @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.project new file mode 100644 index 000000000..d4e08864e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.project @@ -0,0 +1,23 @@ + + + demo-lib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..abec6ca38 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.settings/org.eclipse.m2e.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 000000000..f897a7f1c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/pom.xml new file mode 100644 index 000000000..f1b59671e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/pom.xml @@ -0,0 +1,7 @@ + + 4.0.0 + org.demo + demo-lib + 0.0.1 + Simple demolib to use as a dependency + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/main/java/org/demo/lib/TheLib.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/main/java/org/demo/lib/TheLib.java new file mode 100644 index 000000000..0ea19dd06 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/main/java/org/demo/lib/TheLib.java @@ -0,0 +1,9 @@ +package org.demo.lib; + +public class TheLib { + + public static String createGreeting(String name) { + return "Hello, "+name+"!"; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/main/resources/.empty-folder b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/main/resources/.empty-folder new file mode 100644 index 000000000..0a4c5cd95 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/main/resources/.empty-folder @@ -0,0 +1 @@ +This file is here only because git doesn't represent empty folders \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/test/java/.empty-folder b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/test/java/.empty-folder new file mode 100644 index 000000000..0a4c5cd95 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/test/java/.empty-folder @@ -0,0 +1 @@ +This file is here only because git doesn't represent empty folders \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/test/resources/.empty-folder b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/test/resources/.empty-folder new file mode 100644 index 000000000..0a4c5cd95 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/demo-lib/src/test/resources/.empty-folder @@ -0,0 +1 @@ +This file is here only because git doesn't represent empty folders \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/testapp.zip b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/testapp.zip new file mode 100644 index 000000000..10ab12985 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.test/workspace/testapp.zip differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.classpath new file mode 100644 index 000000000..eca7bdba8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.project new file mode 100644 index 000000000..f9e525d68 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.project @@ -0,0 +1,39 @@ + + + org.springframework.ide.eclipse.boot.dash + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + + + 1432237108310 + + 10 + + org.eclipse.ui.ide.multiFilter + 1.0-projectRelativePath-matches-false-false-target + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..37b12018f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,110 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=info +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.eclipse.jdt.ui.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..a15aefcfd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,60 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=false +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=false +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_type_arguments=true +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=false +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true +sp_cleanup.use_type_arguments=false diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.springframework.ide.eclipse.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.springframework.ide.eclipse.prefs new file mode 100644 index 000000000..a12794d68 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.springframework.ide.eclipse.prefs @@ -0,0 +1,2 @@ +boot.validation.initialized=true +eclipse.preferences.version=1 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/META-INF/MANIFEST.MF b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/META-INF/MANIFEST.MF new file mode 100644 index 000000000..d26107f02 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/META-INF/MANIFEST.MF @@ -0,0 +1,89 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Spring Boot Dashboard View +Bundle-SymbolicName: org.springframework.ide.eclipse.boot.dash;singleton:=true +Bundle-Version: 4.8.1.qualifier +Bundle-Activator: org.springframework.ide.eclipse.boot.dash.BootDashActivator +Bundle-Vendor: Pivotal Inc +Require-Bundle: org.eclipse.ui, + org.springsource.ide.eclipse.commons.ui, + org.eclipse.core.resources, + org.springsource.ide.eclipse.commons.livexp;bundle-version="3.8.4", + org.springframework.ide.eclipse.boot, + org.eclipse.core.expressions, + org.eclipse.ui.ide, + org.eclipse.jdt.ui, + org.eclipse.jdt.core, + org.springsource.ide.eclipse.commons.frameworks.core, + org.eclipse.debug.core, + org.springframework.ide.eclipse.boot.launch, + org.eclipse.jdt.launching, + org.eclipse.jdt.debug.ui, + org.eclipse.ui.console, + org.eclipse.debug.ui, + org.eclipse.ui.forms, + org.json, + com.google.guava;bundle-version="15.0.0", + org.eclipse.equinox.security, + org.eclipse.ui.views.properties.tabbed, + org.eclipse.ui.editors, + org.eclipse.jface.text, + org.dadacoalition.yedit, + org.eclipse.ui.workbench, + org.yaml.snakeyaml, + org.eclipse.compare, + com.fasterxml.jackson.core.jackson-databind, + com.fasterxml.jackson.core.jackson-core, + io.projectreactor.reactor-core;bundle-version="[3.3.1,3.3.2)", + org.reactivestreams.reactive-streams;bundle-version="1.0.3", + org.slf4j.api;bundle-version="1.7.2", + org.eclipse.core.net, + org.apache.httpcomponents.httpclient, + org.apache.httpcomponents.httpcore, + org.springsource.ide.eclipse.commons.core, + org.apache.commons.lang3, + org.springframework.ide.eclipse.editor.support, + org.springframework.ide.eclipse.beans.ui.live, + org.eclipse.core.commands, + org.eclipse.ui.genericeditor, + org.eclipse.e4.ui.css.swt.theme, + org.eclipse.lsp4e, + org.eclipse.lsp4j, + com.google.gson, + org.eclipse.jdt.debug, + org.springsource.ide.eclipse.commons.boot.ls +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-ClassPath: . +Export-Package: org.springframework.ide.eclipse.boot.dash, + org.springframework.ide.eclipse.boot.dash.api, + org.springframework.ide.eclipse.boot.dash.cloudfoundry, + org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment, + org.springframework.ide.eclipse.boot.dash.console, + org.springframework.ide.eclipse.boot.dash.debug, + org.springframework.ide.eclipse.boot.dash.devtools, + org.springframework.ide.eclipse.boot.dash.di, + org.springframework.ide.eclipse.boot.dash.dialogs, + org.springframework.ide.eclipse.boot.dash.labels, + org.springframework.ide.eclipse.boot.dash.liveprocess, + org.springframework.ide.eclipse.boot.dash.livexp, + org.springframework.ide.eclipse.boot.dash.metadata, + org.springframework.ide.eclipse.boot.dash.model, + org.springframework.ide.eclipse.boot.dash.model.actuator, + org.springframework.ide.eclipse.boot.dash.model.actuator.env, + org.springframework.ide.eclipse.boot.dash.model.local, + org.springframework.ide.eclipse.boot.dash.model.remote, + org.springframework.ide.eclipse.boot.dash.model.runtargettypes, + org.springframework.ide.eclipse.boot.dash.ngrok, + org.springframework.ide.eclipse.boot.dash.util, + org.springframework.ide.eclipse.boot.dash.util.template, + org.springframework.ide.eclipse.boot.dash.views, + org.springframework.ide.eclipse.boot.dash.views.properties, + org.springframework.ide.eclipse.boot.dash.views.sections +Import-Package: org.eclipse.core.runtime, + org.eclipse.core.runtime.jobs, + org.eclipse.core.runtime.preferences, + org.osgi.framework, + org.osgi.service.prefs, + org.yaml.snakeyaml +Automatic-Module-Name: org.springframework.ide.eclipse.boot.dash diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/META-INF/p2.inf b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/META-INF/p2.inf new file mode 100644 index 000000000..20c90a573 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/META-INF/p2.inf @@ -0,0 +1,14 @@ +#instructions.configure=\ +# org.eclipse.equinox.p2.touchpoint.eclipse.addRepository(type:0,location:https${#58}//download.eclipse.org/releases/something2,name:Something2);\ +# org.eclipse.equinox.p2.touchpoint.eclipse.uninstallBundle(bundle:org.slf4j.api); + + +#requires.0.namespace = org.eclipse.equinox.p2.iu +#requires.0.name = org.slf4j.api +#requires.0.range = [1.7.2, 1.7.30) +#requires.0.min = 0 +#requires.0.max = 0 + +#units.0.id = org.slf4j.api +#units.0.version = 1.7.2 +#units.0.instructions.install = uninstallBundle(bundle:org.slf4j.api); diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/about.html b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/about.html new file mode 100644 index 000000000..55dfadc68 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/about.html @@ -0,0 +1,248 @@ + + + + +About Spring IDE + + + +

About Spring IDE

+

March 26, 2007

+

Abstract

+

Spring IDE is a set of plugins which provide a user interface for +The Spring Framework's bean +factory XML configuration files including support for Spring 2.0 +namespaces, AOP configuration and Spring Web Flow.

+

License

+ +

The Spring IDE Project makes available all content in this +plug-in ("Content"). Unless otherwise indicated below, the +Content is provided to you under the terms and conditions of the Eclipse +Public License Version 1.0 ("EPL"). A copy of the EPL is +available at https://www.eclipse.org/legal/epl-v10.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the Spring IDE +Project, the Content is being redistributed by another party +("Redistributor") and different terms and conditions may apply +to your use of any object code in the Content. Check the Redistributor's +license that was provided with the Content. If no such license exists, +contact the Redistributor. Unless otherwise indicated below, the terms +and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at https://www.eclipse.org.

+ +

Third Party Content

+

The Content includes items that have been sourced from third +parties as set out below. If you did not receive this Content directly +from the Spring IDE Project, the following is provided for informational +purposes only, and you should look to the Redistributor's license for +terms and conditions of use.

+

The Spring Framework v3.0.5 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Web Flow v2.0.7.A +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Web Services v1.5.8 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Security v3.0.2 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Dynamic Modules for OSGi(TM) Runtimes v2.0.0.M2 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Batch v2.1.0 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Commons Codec v1.3.0 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

Commons Collections v3.2.0 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

Commons Logging v1.1.1 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

Apache MyFaces v1.2.2 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

AspectJ Weaver v1.6.10 +

This product includes software developed by The Eclipse Software +Foundation (https://www.eclipse.org/).

+ +

Antlr v3.0.1 +

Spring IDE includes a binary version of Antlr v3.0.1 (https://www.antlr.org/). +The source code for Antlr is available from the Antlr download site at +https://www.antlr.org/download.html. + +

The Antlr license is available at https://www.antlr.org/license.html. +The license is also reproduced here:

+ +
Copyright (c) 2003-2007, Terence Parr
+All rights reserved.
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions 
+and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions 
+and the following disclaimer in the documentation and/or other materials provided with the distribution.
+Neither the name of the author nor the names of its contributors may be used to endorse or promote 
+products derived from this software without specific prior written permission.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+

+ + +

ASM v2.2.3 +

Spring IDE includes a binary version of ASM v2.2.3 (https://asm.objectweb.org/). +The source code for ASM is available from the ObjectWeb download site at +https://asm.objectweb.org/download/. + +

The ASM license is available at https://asm.objectweb.org/license.html. +The license is also reproduced here:

+ +
Copyright (c) 2000-2005 INRIA, France Telecom
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holders nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
+

+ + +

backport-util-concurrent 3.1.0 +

Spring IDE includes a binary version of backport-util-concurrent v3.1.0 (https://dcl.mathcs.emory.edu/util/backport-util-concurrent/). +The source code for backport-util-concurrent is available from the download site at +https://dcl.mathcs.emory.edu/util/backport-util-concurrent/dist/. + + +

The backport-util-concurrent license is available at https://creativecommons.org/licenses/publicdomain. +The license is also reproduced here:

+ +
Copyright-Only Dedication (based on United States law) or Public Domain Certification
+
+The person or persons who have associated work with this document (the 
+"Dedicator" or "Certifier") hereby either (a) certifies that, to the 
+best of his knowledge, the work of authorship identified is in the public 
+domain of the country from which the work is published, or (b) hereby 
+dedicates whatever copyright the dedicators holds in the work of authorship 
+identified below (the "Work") to the public domain. A certifier, moreover, 
+dedicates any copyright interest he may have in the associated work, and 
+for these purposes, is described as a "dedicator" below.
+
+A certifier has taken reasonable steps to verify the copyright status of 
+this work. Certifier recognizes that his good faith efforts may not shield 
+him from liability if in fact the work certified is not in the public domain.
+
+Dedicator makes this dedication for the benefit of the public at large and 
+to the detriment of the Dedicator's heirs and successors. Dedicator intends 
+this dedication to be an overt act of relinquishment in perpetuity of all 
+present and future rights under copyright law, whether vested or contingent, 
+in the Work. Dedicator understands that such relinquishment of all rights 
+includes the relinquishment of all rights to enforce (by lawsuit or otherwise) 
+those copyrights in the Work.
+
+Dedicator recognizes that, once placed in the public domain, the Work may be 
+freely reproduced, distributed, transmitted, used, modified, built upon, or 
+otherwise exploited by anyone for any purpose, commercial or non-commercial, 
+and in any way, including by methods that have not yet been invented or 
+conceived.
+

+ +

AOP Alliance +

LICENCE: all the source code provided by AOP Alliance is Public Domain. +

+ +

javax.el v1.0.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.jms v1.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.servlet v2.5.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.servlet.jsp v2.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.servlet.jsp.jstl v1.2.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.activation v1.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.annotation v1.0.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.mail v1.4.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.xml.stream v1.0.1 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.xml.ws v2.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

+ + +
Copyright (c) 2005, 2011 Spring IDE Developers
+ + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/build.properties b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/build.properties new file mode 100644 index 000000000..92edeeb7c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/build.properties @@ -0,0 +1,9 @@ +source.. = src/ +output.. = bin/ +bin.includes = plugin.xml,\ + META-INF/,\ + .,\ + icons/,\ + about.html,\ + css/,\ + schema/ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/css/dark-theme.css b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/css/dark-theme.css new file mode 100644 index 000000000..291888b64 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/css/dark-theme.css @@ -0,0 +1,7 @@ +/* See bug 466075 about the pseudo-selector ":org-springframework-ide-eclipse-boot-dash" */ +IEclipsePreferences#org-eclipse-ui-workbench:org-springframework-ide-eclipse-boot-dash { + preferences: + 'org.springframework.ide.eclipse.boot.dash.TextDecorColor=144,238,144' + 'org.springframework.ide.eclipse.boot.dash.AltTextDecorColor=173,216,230', + 'org.springframework.ide.eclipse.boot.dash.MutedTextDecorColor=211,211,211' +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/error_ovr.svg b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/error_ovr.svg new file mode 100644 index 000000000..4860e781f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/error_ovr.svg @@ -0,0 +1,74 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/inactive.svg b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/inactive.svg new file mode 100644 index 000000000..98573c78b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/inactive.svg @@ -0,0 +1,78 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/lattice-16x16.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/lattice-16x16.png new file mode 100644 index 000000000..103202625 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/lattice-16x16.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/lattice-high-res.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/lattice-high-res.png new file mode 100644 index 000000000..238ec3f92 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/lattice-high-res.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/light-bulb.svg b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/light-bulb.svg new file mode 100644 index 000000000..3c108071c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/light-bulb.svg @@ -0,0 +1,139 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/open_browser_disabled.xcf b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/open_browser_disabled.xcf new file mode 100644 index 000000000..d057fe81c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/open_browser_disabled.xcf differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/paused.svg b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/paused.svg new file mode 100644 index 000000000..121d45e32 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/paused.svg @@ -0,0 +1,82 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/re-bug.xcf b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/re-bug.xcf new file mode 100644 index 000000000..44b7ddb5b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/re-bug.xcf differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/wrench.svg b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/wrench.svg new file mode 100644 index 000000000..48f98c7d6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/wrench.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/DT.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/DT.png new file mode 100644 index 000000000..d41c27a49 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/DT.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/DT@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/DT@2x.png new file mode 100644 index 000000000..83ff40946 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/DT@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target.png new file mode 100644 index 000000000..c6aeae4d4 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target@2x.png new file mode 100644 index 000000000..e70d3b87c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target_disabled.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target_disabled.png new file mode 100644 index 000000000..d3b175212 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target_disabled.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target_disabled@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target_disabled@2x.png new file mode 100644 index 000000000..269321c8a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/add_target_disabled@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/boot-icon.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/boot-icon.png new file mode 100644 index 000000000..de88ef0d5 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/boot-icon.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/boot-icon@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/boot-icon@2x.png new file mode 100644 index 000000000..6aef74256 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/boot-icon@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/check.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/check.png new file mode 100644 index 000000000..66a8091be Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/check.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/check_greyedout.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/check_greyedout.png new file mode 100644 index 000000000..3b952cf88 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/check_greyedout.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-inactive.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-inactive.png new file mode 100644 index 000000000..809e1ed8a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-inactive.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-inactive@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-inactive@2x.png new file mode 100644 index 000000000..204670650 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-inactive@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-ready.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-ready.png new file mode 100644 index 000000000..17fad77fa Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-ready.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-ready@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-ready@2x.png new file mode 100644 index 000000000..53e0c26c8 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud-ready@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud_obj.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud_obj.png new file mode 100644 index 000000000..7633c35fb Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/cloud_obj.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/copy.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/copy.png new file mode 100644 index 000000000..71ee89edb Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/copy.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/copy@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/copy@2x.png new file mode 100644 index 000000000..979bfe13f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/copy@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/debug-on-cloud.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/debug-on-cloud.png new file mode 100644 index 000000000..21852015e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/debug-on-cloud.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app.png new file mode 100644 index 000000000..5f0038585 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app@2x.png new file mode 100644 index 000000000..eb2fc720b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app_disabled.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app_disabled.png new file mode 100644 index 000000000..3a79365b7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app_disabled.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app_disabled@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app_disabled@2x.png new file mode 100644 index 000000000..af7ffe248 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/delete_app_disabled@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/error_ovr.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/error_ovr.gif new file mode 100644 index 000000000..119dcccd5 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/error_ovr.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter.png new file mode 100644 index 000000000..27c8738a7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter@2x.png new file mode 100644 index 000000000..779bc07b1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter_disabled.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter_disabled.png new file mode 100644 index 000000000..3c39527ab Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter_disabled.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter_disabled@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter_disabled@2x.png new file mode 100644 index 000000000..9d482043c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/filter_disabled@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/lattice-16x16.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/lattice-16x16.png new file mode 100644 index 000000000..103202625 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/lattice-16x16.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/lattice-wizard-icon.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/lattice-wizard-icon.png new file mode 100644 index 000000000..62459e360 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/lattice-wizard-icon.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb-disabled.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb-disabled.png new file mode 100644 index 000000000..8a71c42a9 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb-disabled.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb-disabled@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb-disabled@2x.png new file mode 100644 index 000000000..1a0d970a6 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb-disabled@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb.png new file mode 100644 index 000000000..1a5fa1d69 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb@2x.png new file mode 100644 index 000000000..d0a90da3d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/light-bulb@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/link_to_editor.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/link_to_editor.png new file mode 100644 index 000000000..c517b3fb3 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/link_to_editor.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/link_to_editor@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/link_to_editor@2x.png new file mode 100644 index 000000000..cf3cd26a9 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/link_to_editor@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_browser.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_browser.png new file mode 100644 index 000000000..b7dcb7982 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_browser.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_browser@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_browser@2x.png new file mode 100644 index 000000000..39643e412 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_browser@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_browser_disabled.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_browser_disabled.gif new file mode 100644 index 000000000..1b5205d98 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_browser_disabled.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console.png new file mode 100644 index 000000000..ca77aee5b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console@2x.png new file mode 100644 index 000000000..54ecae20f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console_disabled.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console_disabled.png new file mode 100644 index 000000000..0c74a7b19 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console_disabled.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console_disabled@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console_disabled@2x.png new file mode 100644 index 000000000..38bb43131 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/open_console_disabled@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/package.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/package.png new file mode 100644 index 000000000..e19a82ed0 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/package.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/package@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/package@2x.png new file mode 100644 index 000000000..ba61a23ae Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/package@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/prop_ps.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/prop_ps.png new file mode 100644 index 000000000..e2428771c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/prop_ps.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/prop_ps@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/prop_ps@2x.png new file mode 100644 index 000000000..6b826fe6c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/prop_ps@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug.png new file mode 100644 index 000000000..ba7ebde30 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug@2x.png new file mode 100644 index 000000000..7be403a47 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug_disabled.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug_disabled.png new file mode 100644 index 000000000..25d937672 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug_disabled.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug_disabled@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug_disabled@2x.png new file mode 100644 index 000000000..c9fc42c87 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rebug_disabled@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/refresh.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/refresh.png new file mode 100644 index 000000000..3de2d8a11 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/refresh.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/refresh@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/refresh@2x.png new file mode 100644 index 000000000..b8ea425fe Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/refresh@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target.png new file mode 100644 index 000000000..5f0038585 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target@2x.png new file mode 100644 index 000000000..eb2fc720b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target_disabled.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target_disabled.png new file mode 100644 index 000000000..3a79365b7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target_disabled.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target_disabled@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target_disabled@2x.png new file mode 100644 index 000000000..af7ffe248 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/remove_target_disabled@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart.png new file mode 100644 index 000000000..08917c61a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart@2x.png new file mode 100644 index 000000000..ae6f399d7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart_disabled.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart_disabled.png new file mode 100644 index 000000000..fd1859c1d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart_disabled.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart_disabled@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart_disabled@2x.png new file mode 100644 index 000000000..0de4b7be5 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/restart_disabled@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/resume.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/resume.gif new file mode 100644 index 000000000..16f4e2517 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/resume.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/resumed.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/resumed.gif new file mode 100644 index 000000000..3e130f546 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/resumed.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_crashed.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_crashed.gif new file mode 100644 index 000000000..486faff8a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_crashed.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_debugging.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_debugging.png new file mode 100644 index 000000000..7d7f79501 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_debugging.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_debugging@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_debugging@2x.png new file mode 100644 index 000000000..8b9df1b11 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_debugging@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_flapping.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_flapping.gif new file mode 100644 index 000000000..609dbb726 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_flapping.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_inactive.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_inactive.png new file mode 100644 index 000000000..c981f1e76 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_inactive.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_inactive@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_inactive@2x.png new file mode 100644 index 000000000..dab1cb05c Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_inactive@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_paused.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_paused.png new file mode 100644 index 000000000..4dd9e03d2 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_paused.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_running.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_running.png new file mode 100644 index 000000000..193444f36 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_running.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_1.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_1.png new file mode 100644 index 000000000..4189201f7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_1.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_1@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_1@2x.png new file mode 100644 index 000000000..4beac7760 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_1@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_2.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_2.png new file mode 100644 index 000000000..a546b16b3 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_2.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_2@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_2@2x.png new file mode 100644 index 000000000..d9c3090a5 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_2@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_3.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_3.png new file mode 100644 index 000000000..cd85d1be1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_3.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_3@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_3@2x.png new file mode 100644 index 000000000..0b662e93f Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_3@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_4.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_4.png new file mode 100644 index 000000000..06c478a4e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_4.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_4@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_4@2x.png new file mode 100644 index 000000000..49be74d70 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_4@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_5.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_5.png new file mode 100644 index 000000000..deac2d980 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_5.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_5@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_5@2x.png new file mode 100644 index 000000000..bea898aef Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_5@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_6.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_6.png new file mode 100644 index 000000000..7b935c19a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_6.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_6@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_6@2x.png new file mode 100644 index 000000000..22e82a8e6 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_6@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_7.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_7.png new file mode 100644 index 000000000..b8d1232ae Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_7.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_7@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_7@2x.png new file mode 100644 index 000000000..bcdbf6902 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_7@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_8.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_8.png new file mode 100644 index 000000000..e9154820e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_8.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_8@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_8@2x.png new file mode 100644 index 000000000..22263a781 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_starting_8@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_unknown.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_unknown.gif new file mode 100644 index 000000000..a54ccfe36 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/rs_unknown.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/run-on-cloud.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/run-on-cloud.png new file mode 100644 index 000000000..5eb1698db Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/run-on-cloud.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/selectmanifest.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/selectmanifest.gif new file mode 100644 index 000000000..484d8280a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/selectmanifest.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/service-inactive.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/service-inactive.png new file mode 100644 index 000000000..794b52b62 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/service-inactive.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/service.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/service.png new file mode 100644 index 000000000..3de91ccd7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/service.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop.png new file mode 100644 index 000000000..3544673b6 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop@2x.png new file mode 100644 index 000000000..b605e00f4 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop_disabled.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop_disabled.png new file mode 100644 index 000000000..e0122f22e Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop_disabled.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop_disabled@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop_disabled@2x.png new file mode 100644 index 000000000..fbcc0526d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/stop_disabled@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/suspend.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/suspend.gif new file mode 100644 index 000000000..cd705c23d Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/suspend.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/suspend_disabled.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/suspend_disabled.gif new file mode 100644 index 000000000..2c245695b Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/suspend_disabled.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/update_password.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/update_password.png new file mode 100644 index 000000000..a85100479 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/update_password.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/update_password@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/update_password@2x.png new file mode 100644 index 000000000..f0c1fded4 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/update_password@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/waiting_ovr.gif b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/waiting_ovr.gif new file mode 100644 index 000000000..7fe247650 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/waiting_ovr.gif differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/warning_ovr.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/warning_ovr.png new file mode 100644 index 000000000..568af1a41 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/warning_ovr.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/warning_ovr@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/warning_ovr@2x.png new file mode 100644 index 000000000..bfb0360c1 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/warning_ovr@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/wizban_cloudfoundry.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/wizban_cloudfoundry.png new file mode 100644 index 000000000..340d84b0a Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/wizban_cloudfoundry.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj.png new file mode 100644 index 000000000..061f2e906 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj@2x.png new file mode 100644 index 000000000..b1c1d8501 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj_disabled.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj_disabled.png new file mode 100644 index 000000000..a6dab93dd Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj_disabled.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj_disabled@2x.png b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj_disabled@2x.png new file mode 100644 index 000000000..b23b289b7 Binary files /dev/null and b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons/write_obj_disabled@2x.png differ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/plugin.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/plugin.xml new file mode 100644 index 000000000..af594d2c0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/plugin.xml @@ -0,0 +1,324 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/pom.xml new file mode 100644 index 000000000..4a083bd25 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + org.springframework.boot.ide + org.springframework.boot.ide + 4.8.1-SNAPSHOT + ../../eclipse-distribution/pom.xml + + + org.springframework.ide.eclipse + org.springframework.ide.eclipse.boot.dash + eclipse-plugin + + + + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + p2 + ignore + + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + second-generate-p2-metadata + + p2-metadata + + verify + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/schema/org.springframework.ide.eclipse.boot.dash.injections.exsd b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/schema/org.springframework.ide.eclipse.boot.dash.injections.exsd new file mode 100644 index 000000000..11f0fb2f8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/schema/org.springframework.ide.eclipse.boot.dash.injections.exsd @@ -0,0 +1,102 @@ + + + + + + + + + [Enter description of this extension point.] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Enter the first release in which this extension point appears.] + + + + + + + + + [Enter extension point usage example here.] + + + + + + + + + [Enter API information here.] + + + + + + + + + [Enter information about supplied implementation of this extension point.] + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/BootDashActivator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/BootDashActivator.java new file mode 100644 index 000000000..4d5640686 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/BootDashActivator.java @@ -0,0 +1,170 @@ +/******************************************************************************* + * 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; + +import java.util.function.Supplier; + +import org.eclipse.core.net.proxy.IProxyService; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.resource.ImageRegistry; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +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.DefaultBootDashModelContext; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.base.Suppliers; + +/** + * The activator class controls the plug-in life cycle + */ +public class BootDashActivator extends AbstractUIPlugin { + + // The plug-in ID + public static final String PLUGIN_ID = "org.springframework.ide.eclipse.boot.dash"; //$NON-NLS-1$ + + public static final String DT_ICON_ID = "devttools"; + public static final String MANIFEST_ICON = "manifest"; + public static final String CLOUD_ICON = "cloud"; + public static final String REFRESH_ICON = "refresh"; + public static final String SERVICE_ICON = "service"; + public static final String SERVICE_INACTIVE_ICON = "service-inactive"; + public static final String BOOT_ICON = "boot"; + public static final String CHECK_ICON = "check"; + public static final String CHECK_GREYSCALE_ICON = "check-greyscale"; + + // The shared instance + private static BootDashActivator plugin; + + private BootDashViewModel model; + + /** + * The constructor + */ + public BootDashActivator() { + } + + private IProxyService proxyService; + + public static final String INJECTIONS_EXTENSION_ID = "org.springframework.ide.eclipse.boot.dash.injections"; + + public synchronized IProxyService getProxyService() { + if (proxyService==null) { + BundleContext bc = getBundle().getBundleContext(); + if (bc!=null) { + ServiceReference sr = bc.getServiceReference(IProxyService.class); + proxyService = bc.getService(sr); + } + } + return proxyService; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework. + * BundleContext) + */ + public void start(BundleContext context) throws Exception { + super.start(context); + plugin = this; + new M2ELogbackCustomizer().schedule(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework. + * BundleContext) + */ + public void stop(BundleContext context) throws Exception { + plugin = null; + super.stop(context); + if (model!=null) { + model.dispose(); + } + } + + /** + * Returns the shared instance + * + * @return the shared instance + */ + public static BootDashActivator getDefault() { + return plugin; + } + + /** + * Returns an image descriptor for the image file at the given plug-in + * relative path + * + * @param path + * the path + * @return the image descriptor + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromPlugin(PLUGIN_ID, path); + } + + /** + * Deprecated. Use static methods in {@link Log} instead. + */ + @Deprecated public static void log(Throwable e) { + Log.log(e); + } + + /** + * Deprecated. Use {@link Log}.warn() instead. + */ + @Deprecated public static void logWarning(String message) { + Log.warn(message); + } + + private final Supplier context = Suppliers.memoize(() -> DefaultBootDashModelContext.create()); + + public synchronized BootDashViewModel getModel() { + if (model==null) { + model = context.get().injections.getBean(BootDashViewModel.class); + +// DebugSelectionListener debugSelectionListener = new DebugSelectionListener(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getSelectionService()); +// model.addDisposableChild(debugSelectionListener); + } + return model; + } + + public SimpleDIContext getInjections() { + return context.get().injections; + } + + @Override + protected void initializeImageRegistry(ImageRegistry reg) { + super.initializeImageRegistry(reg); + reg.put(DT_ICON_ID, getImageDescriptor("/icons/DT.png")); + reg.put(CLOUD_ICON, getImageDescriptor("/icons/cloud_obj.png")); + reg.put(MANIFEST_ICON, getImageDescriptor("icons/selectmanifest.gif")); + reg.put(REFRESH_ICON, getImageDescriptor("/icons/refresh.png")); + reg.put(SERVICE_ICON, getImageDescriptor("icons/service.png")); + reg.put(SERVICE_INACTIVE_ICON, getImageDescriptor("icons/service-inactive.png")); + reg.put(BOOT_ICON, getImageDescriptor("icons/boot-icon.png")); + reg.put(CHECK_ICON, getImageDescriptor("icons/check.png")); + reg.put(CHECK_GREYSCALE_ICON, getImageDescriptor("icons/check_greyedout.png")); + } + + public static IEclipsePreferences getPreferences() { + return InstanceScope.INSTANCE.getNode(PLUGIN_ID); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/M2ELogbackCustomizer.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/M2ELogbackCustomizer.java new file mode 100644 index 000000000..feb9dc2ba --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/M2ELogbackCustomizer.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) 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; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; + +import org.eclipse.core.runtime.IPath; +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.osgi.framework.Bundle; +import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public class M2ELogbackCustomizer extends Job { + + /** + * Snippet to add to the logback.xml file. + */ + private static String SNIPPET = ""; + + public M2ELogbackCustomizer() { + super("M2ELogbackCustomizer"); + setSystem(true); + } + + int retries = 120; + + private boolean isStateLocationInitialized() { + if(!Platform.isRunning()) { + return false; + } + + Bundle resourcesBundle = Platform.getBundle("org.eclipse.core.resources"); + if(resourcesBundle == null) { + return false; + } + + return resourcesBundle.getState() == Bundle.ACTIVE; + } + + @Override + protected IStatus run(IProgressMonitor arg0) { +// Log.info("M2ELogbackCustomizer starting..."); + try { + if (!isStateLocationInitialized()) { +// Log.info("M2ELogbackCustomizer state location not initalized..."); + } else { + Bundle logbackConfigBundle = Platform.getBundle("org.eclipse.m2e.logback.configuration"); +// Log.info("M2ELogbackCustomizer logbackConfigBundle = "+logbackConfigBundle); + String version = logbackConfigBundle.getVersion().toString(); +// Log.info("M2ELogbackCustomizer version = "+version); + IPath statelocationPath = Platform.getStateLocation(logbackConfigBundle); +// Log.info("M2ELogbackCustomizer statelocationPath = "+statelocationPath); + + if (statelocationPath!=null) { + File stateDir = statelocationPath.toFile(); + File logbackFile = new File(stateDir, "logback."+version+".xml"); +// Log.info("M2ELogbackCustomizer logbackFile = "+logbackFile); + if (!logbackFile.isFile()) { +// Log.info("M2ELogbackCustomizer logbackFile is not a file"); + } else { + String logbackConf = IOUtil.toString(new FileInputStream(logbackFile)); + int insertionPoint = logbackConf.indexOf(""); +// Log.info("M2ELogbackCustomizer inseertionPoint = "+insertionPoint); + if (insertionPoint>=0) { + if (logbackConf.contains(SNIPPET)) { + //nothing to do +// Log.info("M2ELogbackCustomizer snippet already present, DONE"); + return Status.OK_STATUS; + } else { +// Log.info("M2ELogbackCustomizer inserting snippet"); + logbackConf = logbackConf.substring(0, insertionPoint) + +SNIPPET+"\n" + logbackConf.substring(insertionPoint); + IOUtil.pipe(new ByteArrayInputStream(logbackConf.getBytes("UTF8")), logbackFile); + } + } + } + } + } + }catch (Exception e) { +// Log.warn(e); + //ignore + } + retry(); + return Status.OK_STATUS; + } + + private void retry() { + if (retries-- > 0) { +// Log.info("M2ELogbackCustomizer will try again later"); + this.schedule(1000); + } else { +// Log.info("M2ELogbackCustomizer no more retry attempts left"); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/ActualInstanceCount.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/ActualInstanceCount.java new file mode 100644 index 000000000..d15b8ce6f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/ActualInstanceCount.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2020 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.api; + +public interface ActualInstanceCount { + int getActualInstances(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/App.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/App.java new file mode 100644 index 000000000..4daccf7aa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/App.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.api; + +import java.util.EnumSet; + +import org.springframework.ide.eclipse.boot.dash.model.Nameable; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; + +public interface App extends Nameable { + default EnumSet supportedGoalStates() { + return EnumSet.noneOf(RunState.class); + } + default void setGoalState(RunState inactive) {} + + RunTarget getTarget(); + + default void setContext(AppContext context) {} + default void restart(RunState runingOrDebugging) {} + + /** + * Override the default naming schema for the console window/view in the + * boot dash ui that shows output for this app. If this method returns null + * default naming schema is used instead. + */ + default String getConsoleDisplayName() { return null; } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/AppConsole.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/AppConsole.java new file mode 100644 index 000000000..46b0a50d9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/AppConsole.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2020 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.api; + +import java.io.IOException; +import java.io.OutputStream; + +import org.springframework.ide.eclipse.boot.dash.console.LogType; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public interface AppConsole { + + void write(String string, LogType stdout) throws Exception ; + + default OutputStream getOutputStream(LogType type) { + return new OutputStream() { + StringBuffer line = new StringBuffer(); + private boolean closed = false; + + @Override + public void write(int b) throws IOException { + if (closed) { + throw new IOException("AppConsole closed"); + } + if (b=='\n') { + try { + AppConsole.this.write(line.toString(), type); + line.delete(0, line.length()); + } catch (IOException e) { + throw e; + } catch (Exception e) { + Log.log(e); + } + } else if (b=='\r') { + } else { + line.append((char)b); + } + } + + @Override + public void close() throws IOException { + this.closed = true; + super.close(); + } + }; + + } + + void show(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/AppConsoleProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/AppConsoleProvider.java new file mode 100644 index 000000000..c645a7a82 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/AppConsoleProvider.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.api; + +/** + * Boot Dash has a console ui that provides a console + * for each {@link App}. The {@link AppConsoleProvider} allows extension authors + * to obtain a reference to the console for their {@link App}s and use it to write + * messages into the console. + */ +public interface AppConsoleProvider { + AppConsole getConsole(App app); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/AppContext.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/AppContext.java new file mode 100644 index 000000000..185702ab2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/AppContext.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2020 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.api; + +import org.springframework.ide.eclipse.boot.dash.model.remote.RefreshStateTracker; + +/** + * AppContext instance is provided to an App by the boot dash via {@link App}.setContext + * method. The app context provides extension point authors easy access to + * the 'context' in which this app is shown in boot dash ui. + */ +public interface AppContext { + + /** + * RefreshStateTracker provides extension point authors a way to execute + * long running operations associated with an app. While the work is + * executing an indication of this, including a 'busy' message will be shown + * somewhere in the boot dash ui. + */ + RefreshStateTracker getRefreshTracker(); + + boolean projectHasDevtoolsDependency(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DebuggableApp.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DebuggableApp.java new file mode 100644 index 000000000..5642a8703 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DebuggableApp.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.api; + +public interface DebuggableApp { + int getDebugPort(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DebuggableTarget.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DebuggableTarget.java new file mode 100644 index 000000000..96b9c59e9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DebuggableTarget.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2020 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.api; + +public interface DebuggableTarget { + boolean isDebuggingSupported(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/Deletable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/Deletable.java new file mode 100644 index 000000000..d57ee5243 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/Deletable.java @@ -0,0 +1,8 @@ +package org.springframework.ide.eclipse.boot.dash.api; + +public interface Deletable { + + default boolean canDelete() { return true; } + void delete() throws Exception; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DesiredInstanceCount.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DesiredInstanceCount.java new file mode 100644 index 000000000..5cc61e7f5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DesiredInstanceCount.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2020 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.api; + +public interface DesiredInstanceCount { + int getDesiredInstances(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DevtoolsConnectable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DevtoolsConnectable.java new file mode 100644 index 000000000..23c73eae2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/DevtoolsConnectable.java @@ -0,0 +1,10 @@ +package org.springframework.ide.eclipse.boot.dash.api; + +public interface DevtoolsConnectable { + String getDevtoolsSecret(); + boolean hasDevtoolsDependency(); + + default TemporalBoolean isDevtoolsConnectable() { + return TemporalBoolean.now(hasDevtoolsDependency() && getDevtoolsSecret()!=null); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/JmxConnectable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/JmxConnectable.java new file mode 100644 index 000000000..9243b4946 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/JmxConnectable.java @@ -0,0 +1,5 @@ +package org.springframework.ide.eclipse.boot.dash.api; + +public interface JmxConnectable { + String getJmxUrl(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/LogConnection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/LogConnection.java new file mode 100644 index 000000000..119a78806 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/LogConnection.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * Copyright (c) 2020 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.api; + +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +public interface LogConnection extends Disposable { + + boolean isClosed(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/LogSource.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/LogSource.java new file mode 100644 index 000000000..66bcff8d4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/LogSource.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2020 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.api; + +/** + * If an {@link App} implements this interface then it represents a source of 'streamed' log + * output. I.e. it generates output over time in background and this output can be + * displayed in boot dash console ui. + * + * This interface is also used as a indicator whether open console action is applicable to an element. + */ +public interface LogSource { + + /** + * Called by boot dash to request a App that is a LogSource to start streaming + * log output to the console. + * + * @param logConsole the console to which output should be streamed. + * @param includeHistory when true, it means before streaming current logs the + * app may also first send historic logs to the console. If an app doesn't + * have the capability to provide historic logs, it can just ignore this + * parameter. + * @return a LogConnection object which allows the boot dashboard to stop + * the log streaming when the console is closed. + */ + LogConnection connectLog(AppConsole logConsole, boolean includeHistory); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/PortConnectable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/PortConnectable.java new file mode 100644 index 000000000..14e52a8de --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/PortConnectable.java @@ -0,0 +1,10 @@ +package org.springframework.ide.eclipse.boot.dash.api; + +import java.util.Set; + +public interface PortConnectable { + + + Set getPorts(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/ProjectDeploymentTarget.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/ProjectDeploymentTarget.java new file mode 100644 index 000000000..9d423409a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/ProjectDeploymentTarget.java @@ -0,0 +1,11 @@ +package org.springframework.ide.eclipse.boot.dash.api; + +import java.util.Set; + +import org.eclipse.core.resources.IProject; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +public interface ProjectDeploymentTarget /* extends RunTarget */ { + void performDeployment(Set of, RunState runOrDebug) throws Exception; +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/ProjectRelatable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/ProjectRelatable.java new file mode 100644 index 000000000..b1c2b25b0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/ProjectRelatable.java @@ -0,0 +1,7 @@ +package org.springframework.ide.eclipse.boot.dash.api; + +import org.eclipse.core.resources.IProject; + +public interface ProjectRelatable { + IProject getProject(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/RunStateIconProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/RunStateIconProvider.java new file mode 100644 index 000000000..d196eb54b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/RunStateIconProvider.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.api; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.springframework.ide.eclipse.boot.dash.model.RunState; + +/** + * A {@link App} instance can implement this interface to override the + * default runstate icons for elements. + */ +public interface RunStateIconProvider { + ImageDescriptor getRunStateIcon(RunState runState); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/RunStateProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/RunStateProvider.java new file mode 100644 index 000000000..f2d5f1f7c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/RunStateProvider.java @@ -0,0 +1,9 @@ +package org.springframework.ide.eclipse.boot.dash.api; + +import org.springframework.ide.eclipse.boot.dash.model.RunState; + +public interface RunStateProvider { + + RunState fetchRunState(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/RunTargetType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/RunTargetType.java new file mode 100644 index 000000000..6bcbd11ce --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/RunTargetType.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright (c) 2015 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.api; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.springframework.ide.eclipse.boot.dash.model.MissingLiveInfoMessages; +import org.springframework.ide.eclipse.boot.dash.model.Nameable; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.util.template.Templates; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; + +/** + * A run target type represents a type of 'deployment environment' to which + * boot apps can be targetted to run. For example 'local', 'cloudfoundry' + * or 'lattice'. + * + * @author Kris De Volder + */ +public interface RunTargetType extends Nameable { + + /** + * @return Whether it is possible to create instances of this type. Not all + * runtargets provide this ability. For example the 'local' run target + * is a singleton and doesn't allow creating instances. + */ + boolean canInstantiate(); + + /** + * RunTargetTypes that return 'true' from 'canCreate' must provide an implementation + * of this method. When called it opens a UI allowing the user to create a new + * run target. + */ + CompletableFuture openTargetCreationUi(LiveSetVariable targets); + + /** + * + * @return if {@link #canInstantiate()} returns true, return a new {@link RunTarget}. Return null if this + * type cannot be instantiated. + */ + RunTarget createRunTarget(Params properties); + + ImageDescriptor getIcon(); + + /** + * Provides a means to store persistent properties associated with this {@link RunTargetType} + */ + IPropertyStore getPropertyStore(); + + /** + * A convenience method that provides access to the persisent property store returned by getPropertyStore + * through more convenient API. + */ + PropertyStoreApi getPersistentProperties(); + + /** + * Provides a Default template (see {@link Templates}) for rendering the name of targets of this type. + * This default can be overriden via a persisent property set on this target. (I.e. the default is + * only used if the property isn't set. + */ + String getDefaultNameTemplate(); + + /** + * Sets a persistent property that overrides the default name template. + */ + void setNameTemplate(String string) throws Exception; + + /** + * Gets the effective name template. May return null if there is neither a defaultNameTemplate nor + * a template provided through setNameTemplate. + */ + String getNameTemplate(); + + /** + * Gets a short, helpful message describing the supported template language (i.e. at least list the + * supported '%' template variables. + */ + String getTemplateHelpText(); + + Params parseParams(String serializedTargetParams); + String serialize(Params serializedTargetParams); + + ImageDescriptor getDisconnectedIcon(); + + default MissingLiveInfoMessages getMissingLiveInfoMessages() { + return MissingLiveInfoMessages.DEFAULT; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/Styleable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/Styleable.java new file mode 100644 index 000000000..c5c27a614 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/Styleable.java @@ -0,0 +1,11 @@ +package org.springframework.ide.eclipse.boot.dash.api; + +import org.eclipse.jface.viewers.StyledString; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +public interface Styleable { + + + StyledString getStyledName(Stylers stylers); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/SystemPropertyProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/SystemPropertyProvider.java new file mode 100644 index 000000000..3b1966fca --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/SystemPropertyProvider.java @@ -0,0 +1,7 @@ +package org.springframework.ide.eclipse.boot.dash.api; + +public interface SystemPropertyProvider { + + String getSystemProperty(String key); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/SystemPropertySupport.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/SystemPropertySupport.java new file mode 100644 index 000000000..3f9c74ea8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/SystemPropertySupport.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2020 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.api; + +/** + * 'App' that can support the setting of system properties to be passed into theit JVM process + * implement this interface to define how to inject these properties into the process + * when it is launched. + */ +public interface SystemPropertySupport extends SystemPropertyProvider { + + /** + * Set property to a value or set it to null to 'erase' the property. + */ + void setSystemProperty(String name, String value); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/TemporalBoolean.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/TemporalBoolean.java new file mode 100644 index 000000000..be8751975 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/api/TemporalBoolean.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2020 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.api; + +public enum TemporalBoolean { + + /** + * True now, but maybe not forever. + */ + TRUE, + /** + * False now, but maybe not forever + */ + FALSE, + + /** + * False now and forever. + */ + NEVER, + + /** + * True now and forever. + */ + ALLwAYS; + + public boolean isTrue() { + return this==TRUE || this==ALLwAYS; + } + + public boolean isFalse() { + return this==FALSE || this==NEVER; + } + + public static TemporalBoolean allways(boolean b) { + return b ? ALLwAYS : NEVER; + } + + public static TemporalBoolean now(boolean b) { + return b ? TRUE : FALSE; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/JavaPackageFragmentRootHandler.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/JavaPackageFragmentRootHandler.java new file mode 100644 index 000000000..214a5926b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/JavaPackageFragmentRootHandler.java @@ -0,0 +1,351 @@ +/******************************************************************************* + * Copyright (c) 2015 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 java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchDelegate; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jdt.launching.IRuntimeClasspathEntry; +import org.eclipse.jdt.launching.JavaLaunchDelegate; +import org.eclipse.jdt.launching.JavaRuntime; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +/** + * Resolves all the package fragment roots and main type for a give Java + * project. It includes handling all required Java projects for the given java + * project. + * + */ +public class JavaPackageFragmentRootHandler { + + private IJavaProject javaProject; + + private IType type; + + public JavaPackageFragmentRootHandler(IJavaProject javaProject) { + this.javaProject = javaProject; + } + + public IPackageFragmentRoot[] getPackageFragmentRoots(IProgressMonitor monitor) throws Exception { + + IPath[] classpathEntries = null; + ILaunchConfiguration configuration = null; + try { + if (BootPropertyTester.isBootProject(javaProject.getProject())) { + configuration = BootLaunchConfigurationDelegate.createConf(javaProject); + Set modes = new HashSet(); + modes.add(ILaunchManager.RUN_MODE); + + ILaunchDelegate[] delegates = configuration.getType().getDelegates(modes); + if (delegates != null && delegates.length > 0) { + + JavaLaunchDelegate javaDelegate = null; + + for (ILaunchDelegate del : delegates) { + if (BootLaunchConfigurationDelegate.TYPE_ID.equals(del.getId()) + && del.getDelegate() instanceof JavaLaunchDelegate) { + + javaDelegate = (JavaLaunchDelegate) del.getDelegate(); + break; + } + } + + if (javaDelegate != null) { + String[] pathValues = javaDelegate.getClasspath(configuration); + if (pathValues != null) { + classpathEntries = new IPath[pathValues.length]; + for (int i = 0; i < pathValues.length && i < classpathEntries.length; i++) { + classpathEntries[i] = new Path(pathValues[i]); + } + } + } + } + + } else { + throw ExceptionUtil.coreException("The project : " + javaProject.getProject().getName() + + " does not appear to be a Spring Boot project. Only Spring Boot projects can be deployed through the Boot Dashboard."); + } + + } finally { + if (configuration != null) { + configuration.delete(); + } + } + + if (classpathEntries != null && classpathEntries.length > 0) { + List pckRoots = new ArrayList(); + + // Since the java project may have other required projects, fetch + // the + // ordered + // list of required projects that will be used to search for package + // fragment roots + // corresponding to the resolved class paths. The order of the java + // projects to search in should + // start with the most immediate list of required projects of the + // java + // project. + List javaProjectsToSearch = getOrderedJavaProjects(javaProject); + + // Find package fragment roots corresponding to the path entries. + // Search through all java projects, not just the immediate java + // project + // for the application that is being pushed to CF. + for (IPath path : classpathEntries) { + + for (IJavaProject javaProject : javaProjectsToSearch) { + + IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots(); + + if (roots != null) { + List foundRoots = new ArrayList(); + + for (IPackageFragmentRoot packageFragmentRoot : roots) { + // Note that a class path entry may correspond to a + // Java project's target directory. + // Different fragment roots may use the same target + // directory, so relationship between fragment roots + // to a class path entry may be many-to-one. + + if (isRootAtEntry(packageFragmentRoot, path)) { + foundRoots.add(packageFragmentRoot); + } + } + + // Stop after the first successful search + if (!foundRoots.isEmpty()) { + pckRoots.addAll(foundRoots); + break; + } + } + } + + } + + return pckRoots.toArray(new IPackageFragmentRoot[pckRoots.size()]); + } else { + return null; + } + } + + /** + * Attempts to resolve a main type in the associated Java project. Throws + * {@link CoreException} if it failed to resolve main type or no main type + * found. + * + * @param monitor + * @return non-null Main type. + * @throws CoreException + * if failure occurred while resolving main type or no main type + * found + */ + public IType getMainType(IProgressMonitor monitor) throws Exception { + if (type == null) { + type = new JavaTypeResolver(javaProject).getMainTypesFromSource(monitor); + if (type == null) { + throw ExceptionUtil.coreException( + "No main type found. Verify that the project can be packaged as a jar application and contains a main type in source. War packaging of projects is not yet supported."); + } + } + return type; + } + + protected List getOrderedJavaProjects(IJavaProject project) { + List collectedProjects = new ArrayList(); + getOrderedJavaProjectNames(Arrays.asList(project.getProject().getName()), collectedProjects); + + List projects = new ArrayList(); + + for (String name : collectedProjects) { + IProject prj = ResourcesPlugin.getWorkspace().getRoot().getProject(name); + if (prj != null) { + IJavaProject jvPrj = JavaCore.create(prj); + if (jvPrj != null && jvPrj.exists()) { + projects.add(jvPrj); + } + } + } + + return projects; + + } + + protected void getOrderedJavaProjectNames(List sameLevelRequiredProjects, List collectedProjects) { + // The order in which required projects are collected is as follows, + // with the RHS + // being required projects of the LHS + // A -> BC + // B -> D + // C -> E + // = total 5 projects, added in the order that they are encountered. + // so final ordered list should be ABCDE + if (sameLevelRequiredProjects == null) { + return; + } + List nextLevelRequiredProjects = new ArrayList(); + // First add the current level java projects in the order they appear + // and also collect each one's required names. + for (String name : sameLevelRequiredProjects) { + try { + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(name); + if (project != null) { + IJavaProject jvPrj = JavaCore.create(project); + if (jvPrj != null && jvPrj.exists()) { + if (!collectedProjects.contains(name)) { + collectedProjects.add(name); + } + String[] names = jvPrj.getRequiredProjectNames(); + if (names != null && names.length > 0) { + for (String reqName : names) { + if (!nextLevelRequiredProjects.contains(reqName)) { + nextLevelRequiredProjects.add(reqName); + } + } + } + } + } + + } catch (JavaModelException e) { + BootDashActivator.log(e); + } + + } + + // Now recurse to fetch the required projects for the + // list of java projects that were added at the current level above + if (!nextLevelRequiredProjects.isEmpty()) { + getOrderedJavaProjectNames(nextLevelRequiredProjects, collectedProjects); + + } + } + + protected ILaunchConfiguration createConfiguration(IType type) throws Exception { + + ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager(); + + ILaunchConfigurationType configType = manager + .getLaunchConfigurationType(IJavaLaunchConfigurationConstants.ID_JAVA_APPLICATION); + + ILaunchConfigurationWorkingCopy workingCopy = configType.newInstance(null, + manager.generateLaunchConfigurationName(type.getTypeQualifiedName('.'))); + workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, type.getFullyQualifiedName()); + workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, + type.getJavaProject().getElementName()); + workingCopy.setMappedResources(new IResource[] { type.getUnderlyingResource() }); + return workingCopy; + } + + protected IPath[] getRuntimeClasspaths(ILaunchConfiguration configuration) throws Exception { + IRuntimeClasspathEntry[] entries = JavaRuntime.computeUnresolvedRuntimeClasspath(configuration); + entries = JavaRuntime.resolveRuntimeClasspath(entries, configuration); + + ArrayList userEntries = new ArrayList(entries.length); + for (int i = 0; i < entries.length; i++) { + if (entries[i].getClasspathProperty() == IRuntimeClasspathEntry.USER_CLASSES) { + + String location = entries[i].getLocation(); + if (location != null) { + IPath entry = Path.fromOSString(location); + if (!userEntries.contains(entry)) { + userEntries.add(entry); + } + } + } + } + return userEntries.toArray(new IPath[userEntries.size()]); + } + + /** + * + * Determines if the given package fragment root corresponds to the class + * path entry path. + *

+ * Note that different package fragment roots may point to the same class + * path entry. + *

+ * Example: + *

+ * A Java project may have the following package fragment roots: + *

+ * - src/main/java + *

+ * - src/main/resources + *

+ * Both may be using the same output folder: + *

+ * target/classes. + *

+ * In this case, the output folder will have a class path entry - + * target/classes - and it will be the same for both roots, and this method + * will return true for both roots if passed the entry for target/classes + * + * @param root + * to check if it corresponds to the given class path entry path + * @param entry + * @return true if root is at the given entry + */ + private static boolean isRootAtEntry(IPackageFragmentRoot root, IPath entry) { + try { + IClasspathEntry cpe = root.getRawClasspathEntry(); + if (cpe.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + IPath outputLocation = cpe.getOutputLocation(); + if (outputLocation == null) { + outputLocation = root.getJavaProject().getOutputLocation(); + } + + IPath location = ResourcesPlugin.getWorkspace().getRoot().findMember(outputLocation).getLocation(); + if (entry.equals(location)) { + return true; + } + } + } catch (JavaModelException e) { + BootDashActivator.log(e); + } + + IResource resource = root.getResource(); + if (resource != null && entry.equals(resource.getLocation())) { + return true; + } + + IPath path = root.getPath(); + if (path != null && entry.equals(path)) { + return true; + } + + return false; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/JavaTypeResolver.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/JavaTypeResolver.java new file mode 100644 index 000000000..f0f1eec29 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/JavaTypeResolver.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2015 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 java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jface.window.Window; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.springframework.ide.eclipse.boot.dash.util.UiUtil; +import org.springsource.ide.eclipse.commons.frameworks.core.maintype.MainTypeFinder; + +/** + * + * Helper methods for UI components that require Java type searching, given a + * valid java project. + */ +public class JavaTypeResolver { + + private final IJavaProject project; + + public JavaTypeResolver(IJavaProject project) { + this.project = project; + } + + protected IJavaProject getJavaProject() { + return project; + } + + public IType[] getMainTypes(IProgressMonitor monitor) throws Exception { + return MainTypeFinder.guessMainTypes(getJavaProject(), monitor); + + } + + public IType getMainTypesFromSource(IProgressMonitor monitor) throws Exception { + if (project != null) { + IType[] types = getMainTypes(monitor); + // Enable when dependency to + // org.springsource.ide.eclipse.commons.core is + // added. This should be the common way to obtain main types + // MainTypeFinder.guessMainTypes(project, monitor); + + if (types != null && types.length > 0) { + + final List typesFromSource = new ArrayList(); + + for (IType type : types) { + if (!type.isBinary() && !typesFromSource.contains(type)) { + typesFromSource.add(type); + } + } + + if (typesFromSource.size() == 1) { + return typesFromSource.get(0); + } else if (typesFromSource.size() > 1) { + // Prompt user to select a main type + + final IType[] selectedType = new IType[1]; + Display.getDefault().syncExec(new Runnable() { + + @Override + public void run() { + + final Shell shell = UiUtil.getShell(); + + if (shell != null && !shell.isDisposed()) { + SelectMainTypeWizard wizard = new SelectMainTypeWizard(typesFromSource); + WizardDialog dialog = new WizardDialog(shell, wizard); + if (dialog.open() == Window.OK) { + selectedType[0] = wizard.getSelectedMainType(); + } + + } else { + selectedType[0] = typesFromSource.get(0); + } + } + }); + return selectedType[0]; + } + } + } + return null; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/JmxSshTunnelStatus.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/JmxSshTunnelStatus.java new file mode 100644 index 000000000..29b856f45 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/JmxSshTunnelStatus.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2018 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; + +public enum JmxSshTunnelStatus { + + /** + * Tunneling is not enabled. + */ + DISABLED, + + /** + * Tunneling is enabled, but the tunnel does not (yet) exist. + */ + INACTIVE, + + /** + * Tunneling is enabled and a active tunnel currently exists. + */ + ACTIVE; + + /** + * Nice label for display in ui. + */ + public String getLabel() { + switch (this) { + case DISABLED: + return "Disabled"; + case INACTIVE: + return "Enabled - SSH Tunnel not (yet) created"; + case ACTIVE: + return "Enabled - SSH Tunnel is Active"; + default: + throw new IllegalStateException("Missing switch case "+this); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/MissingPasswordException.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/MissingPasswordException.java new file mode 100644 index 000000000..5e71db865 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/MissingPasswordException.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2015 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; + +/** + * Exception for missing password + * + * @author Alex Boyko + * + */ +public class MissingPasswordException extends Exception { + + /** + * Serial version id. + */ + private static final long serialVersionUID = 1L; + + public MissingPasswordException(String message) { + super(message); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/OperationTracker.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/OperationTracker.java new file mode 100644 index 000000000..70a1e1859 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/OperationTracker.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2016 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 java.util.function.Supplier; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.Platform; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; +import org.springsource.ide.eclipse.commons.livexp.core.LiveCounter; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + + +/** + * Keeps track of whether a certain 'operation' is currently in progress. + * + * @author Kris De Volder + */ +public class OperationTracker { + + private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder"); + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + @FunctionalInterface + public interface Task { + void run() throws Exception; + } + + private LiveVariable error; + + /** + * Counter that keeps a count of the number of 'nested' operations are + * currently in progress (i.e. operation was started but not yet ended) + */ + public final LiveCounter inProgress = new LiveCounter(); + + private Supplier name; + + public OperationTracker(Supplier name, LiveVariable error) { + this.name = name; + this.error = error; + } + + private void start() { + setError(null); + inProgress.increment(); + debug("starting: "+name.get()+" ["+inProgress.getValue()+"]"); + } + + private void setError(Throwable e) { + error.setValue(e); + } + + public void whileExecuting(UserInteractions ui, CancelationToken cancelationToken, IProgressMonitor monitor, Task task) throws Exception { + Throwable error = null; + start(); + try { + task.run(); + } catch (Throwable e) { + error = e; + } finally { + end(error, ui, cancelationToken, monitor); + } + } + + private void end(Throwable error, UserInteractions ui, CancelationToken cancelationToken, IProgressMonitor monitor) throws Exception { + Assert.isLegal(inProgress.getValue()>0); + int level = inProgress.decrement(); + debug("ended: "+name.get()+" ["+inProgress.getValue()+"]"); + if (cancelationToken.isCanceled() || monitor.isCanceled()) { + //Avoid setting error results for canceled operation. If an op is canceled + // its errors should simply be ignored. + throw new OperationCanceledException(); + } + if (level==0 && !(ExceptionUtil.isCancelation(error))) { + setError(error); + } + if (error != null) { + throw ExceptionUtil.exception(error); + } + } + + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/RemoteBootDashModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/RemoteBootDashModel.java new file mode 100644 index 000000000..e3799a8c0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/RemoteBootDashModel.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * 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 java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.jdt.core.IJavaProject; +import org.springframework.ide.eclipse.boot.dash.api.ProjectDeploymentTarget; +import org.springframework.ide.eclipse.boot.dash.model.AbstractBootDashModel; +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.ModifiableModel; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.remote.RefreshStateTracker; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTarget.ConnectMode; +import org.springframework.ide.eclipse.boot.dash.views.BootDashModelConsoleManager; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; + +@SuppressWarnings("rawtypes") +public abstract class RemoteBootDashModel extends AbstractBootDashModel implements ModifiableModel, ProjectDeploymentTarget { + + protected static final String AUTO_CONNECT_PROP = GenericRemoteBootDashModel.class.getSimpleName()+".auto-connect"; + + public final RefreshStateTracker refreshTracker = new RefreshStateTracker(this); + + public RemoteBootDashModel(RunTarget target, BootDashViewModel parent) { + super(target, parent); + BootDashModelContext context = parent.getContext(); + getRunTarget().getClientExp().onChange(this, (e, v) -> { + RemoteBootDashModel.this.notifyModelStateChanged(); + RemoteBootDashModel.this.getViewModel().updateTargetPropertiesInStore(); + }); + refreshTracker.refreshState.onChange(this, (e, v) -> { + RemoteBootDashModel.this.notifyModelStateChanged(); + }); + addDisposableChild(refreshTracker.refreshState); + if (getRunTarget().getPersistentProperties().get(AUTO_CONNECT_PROP, true)) { + if (!getRunTarget().isConnected()) { + Log.async(connect(ConnectMode.AUTOMATIC)); + } + } + } + + public final IPropertyStore getPropertyStore() { + return getRunTarget().getPropertyStore(); + } + + @Override + public RemoteRunTarget getRunTarget() { + return (RemoteRunTarget) super.getRunTarget(); + } + + final public CompletableFuture connect(ConnectMode mode) { + return refreshTracker.callAsync("Connecting...", () -> { + try { + getRunTarget().connect(mode); + } catch (Exception e) { + if (mode==ConnectMode.INTERACTIVE) { + ui().errorPopup("Failed to connect to " + getDisplayName() + ". ", ExceptionUtil.getMessage(e)); + } + throw e; + } + getRunTarget().getPersistentProperties().put(AUTO_CONNECT_PROP, true); + return null; + }); + } + + final public void disconnect() { + try { + getRunTarget().getPersistentProperties().put(AUTO_CONNECT_PROP, false); + } catch (Exception e) { + Log.log(e); + } + getRunTarget().disconnect(); + } + + @Override + public final void add(List sources) throws Exception { + Builder projects = ImmutableSet.builder(); + if (sources != null) { + for (Object obj : sources) { + IProject project = getProject(obj); + if (project != null) { + projects.add(project); + } + } + performDeployment(projects.build(), RunState.RUNNING); + } + } + + final 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; + } + + + @Override + final public BootDashModelConsoleManager getElementConsoleManager() { + return injections().getBean(BootDashModelConsoleManager.class); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/SelectMainTypeWizard.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/SelectMainTypeWizard.java new file mode 100644 index 000000000..262c69aee --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/SelectMainTypeWizard.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2015 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 java.util.List; + +import org.eclipse.jdt.core.IType; +import org.eclipse.jface.wizard.Wizard; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; + +/** + * + */ +public class SelectMainTypeWizard extends Wizard { + + private final List mainTypes; + + private SelectMainTypeWizardPage page; + + public SelectMainTypeWizard(List mainTypes) { + this.mainTypes = mainTypes; + } + + @Override + public void addPages() { + page = new SelectMainTypeWizardPage(mainTypes, + BootDashActivator.getImageDescriptor("icons/wizban_cloudfoundry.png")); + addPage(page); + } + + public boolean performFinish() { + return page != null && page.getSelectedMainType() != null; + } + + public IType getSelectedMainType() { + return page != null ? page.getSelectedMainType() : null; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/SelectMainTypeWizardPage.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/SelectMainTypeWizardPage.java new file mode 100644 index 000000000..7ebc29541 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/SelectMainTypeWizardPage.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) 2015 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 java.util.List; + +import org.eclipse.jdt.core.IType; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; + +/** + * + */ +public class SelectMainTypeWizardPage extends WizardPage { + + private Combo typeCombo; + + boolean canFinish = false; + + private final List mainTypes; + + private IType selectedMainType; + + protected SelectMainTypeWizardPage(List mainTypes, + ImageDescriptor descriptor) { + super("Select Java Main Type"); + this.mainTypes = mainTypes; + setTitle("Select Java Main Type"); + setDescription("Multiple Java main types found. Please select one."); + + if (descriptor != null) { + setImageDescriptor(descriptor); + } + } + + public void createControl(Composite parent) { + + Composite composite = new Composite(parent, SWT.NONE); + GridDataFactory.fillDefaults().grab(true, false).applyTo(composite); + GridLayoutFactory.fillDefaults().numColumns(1).margins(10, 10) + .applyTo(composite); + + Label mainTypeLabel = new Label(composite, SWT.NONE); + mainTypeLabel.setText("Main type:"); + GridDataFactory.fillDefaults().applyTo(mainTypeLabel); + + typeCombo = new Combo(composite, SWT.BORDER | SWT.READ_ONLY); + + GridDataFactory.fillDefaults().applyTo(typeCombo); + + typeCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + resolveSelection(); + } + }); + + String[] comboItems = new String[mainTypes.size()]; + for (int i = 0; i < comboItems.length && i < mainTypes.size(); i++) { + String name = mainTypes.get(i).getFullyQualifiedName(); + comboItems[i] = name; + } + + typeCombo.setItems(comboItems); + if (mainTypes.size() > 0) { + typeCombo.select(0); + } + resolveSelection(); + Dialog.applyDialogFont(composite); + setControl(composite); + + } + + private void resolveSelection() { + if (typeCombo != null && !typeCombo.isDisposed()) { + int index = typeCombo.getSelectionIndex(); + + if (index >= 0) { + String typeName = typeCombo.getItem(index); + for (IType type : mainTypes) { + if (type.getFullyQualifiedName().equals(typeName)) { + selectedMainType = type; + break; + } + } + } + update(); + } + } + + private void update() { + + setErrorMessage(null); + canFinish = getSelectedMainType() != null; + + if (getWizard() != null && getWizard().getContainer() != null) { + getWizard().getContainer().updateButtons(); + setPageComplete(canFinish); + } + } + + @Override + public boolean isPageComplete() { + return canFinish; + } + + public IType getSelectedMainType() { + return selectedMainType; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/CompositeReconcilingStrategy.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/CompositeReconcilingStrategy.java new file mode 100644 index 000000000..039e88bab --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/CompositeReconcilingStrategy.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * Copyright (c) 2016 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.deployment; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.reconciler.DirtyRegion; +import org.eclipse.jface.text.reconciler.IReconcilingStrategy; +import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension; + +/** + * Reconciling strategy composed of a number of reconciling strategies producing + * various types of annotations that can be seen in different parts of the UI. + * Implementation is based on JDT's CompositeReconcilingStrategy + * + * @author Alex Boyko + * + */ +public class CompositeReconcilingStrategy implements IReconcilingStrategy, IReconcilingStrategyExtension { + + /** The list of internal reconciling strategies. */ + private IReconcilingStrategy[] fStrategies; + + /** + * Creates a new, empty composite reconciling strategy. + */ + public CompositeReconcilingStrategy() { + } + + /** + * Sets the reconciling strategies for this composite strategy. + * + * @param strategies the strategies to be set or null + */ + public void setReconcilingStrategies(IReconcilingStrategy[] strategies) { + fStrategies= strategies; + } + + /** + * Returns the previously set stratgies or null. + * + * @return the contained strategies or null + */ + public IReconcilingStrategy[] getReconcilingStrategies() { + return fStrategies; + } + + /* + * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#setDocument(org.eclipse.jface.text.IDocument) + */ + @Override + public void setDocument(IDocument document) { + if (fStrategies == null) + return; + + for (int i= 0; i < fStrategies.length; i++) + fStrategies[i].setDocument(document); + } + + /* + * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#reconcile(org.eclipse.jface.text.reconciler.DirtyRegion, org.eclipse.jface.text.IRegion) + */ + @Override + public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) { + if (fStrategies == null) + return; + + for (int i= 0; i < fStrategies.length; i++) + fStrategies[i].reconcile(dirtyRegion, subRegion); + } + + /* + * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#reconcile(org.eclipse.jface.text.IRegion) + */ + @Override + public void reconcile(IRegion partition) { + if (fStrategies == null) + return; + + for (int i= 0; i < fStrategies.length; i++) + fStrategies[i].reconcile(partition); + } + + /* + * @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#setProgressMonitor(org.eclipse.core.runtime.IProgressMonitor) + */ + @Override + public void setProgressMonitor(IProgressMonitor monitor) { + if (fStrategies == null) + return; + + for (int i=0; i < fStrategies.length; i++) { + if (fStrategies[i] instanceof IReconcilingStrategyExtension) { + IReconcilingStrategyExtension extension= (IReconcilingStrategyExtension) fStrategies[i]; + extension.setProgressMonitor(monitor); + } + } + } + + /* + * @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#initialReconcile() + */ + @Override + public void initialReconcile() { + if (fStrategies == null) + return; + + for (int i=0; i < fStrategies.length; i++) { + if (fStrategies[i] instanceof IReconcilingStrategyExtension) { + IReconcilingStrategyExtension extension= (IReconcilingStrategyExtension) fStrategies[i]; + extension.initialReconcile(); + } + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/DeployToRemoteTargetAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/DeployToRemoteTargetAction.java new file mode 100644 index 000000000..56ddd31f9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/DeployToRemoteTargetAction.java @@ -0,0 +1,123 @@ +/******************************************************************************* + * Copyright (c) 2016-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.deployment; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.Assert; +import org.eclipse.swt.widgets.Display; +import org.springframework.ide.eclipse.boot.dash.api.DebuggableTarget; +import org.springframework.ide.eclipse.boot.dash.api.ProjectDeploymentTarget; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.RemoteBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTargetType; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.collect.ImmutableSet; + +/** + * @author Kris De Volder + */ +public class DeployToRemoteTargetAction extends AbstractBootDashElementsAction { + + private RunState runOrDebug; + private RemoteRunTarget target; + private ValueListener connectionListener; + + public DeployToRemoteTargetAction(Params params, RemoteRunTarget target, RunState runningOrDebugging) { + super(params); + this.setText(target.getName()); + Assert.isLegal(target.getType() instanceof RemoteRunTargetType); + Assert.isLegal(runningOrDebugging==RunState.RUNNING || runningOrDebugging==RunState.DEBUGGING); + this.target = target; + this.runOrDebug = runningOrDebugging; + + this.connectionListener = new ValueListener() { + @Override + public void gotValue(LiveExpression exp, Client value) { + Display.getDefault().asyncExec(new Runnable() { + public void run() { + update(); + } + }); + } + }; + target.getClientExp().addListener(connectionListener); + updateEnablement(); + } + + @Override + public void updateVisibility() { + BootDashElement element = getSingleSelectedElement(); + setVisible(element != null && element instanceof BootProjectDashElement); + } + + @Override + public void updateEnablement() { + BootDashElement element = getSingleSelectedElement(); + setVisible(element != null && element instanceof BootProjectDashElement); + + if (this.target != null && this.target instanceof RemoteRunTarget) { + setEnabled(((RemoteRunTarget) this.target).isConnected() && isSupportedMode(element) && targetSupportsDebugging()); + } + } + + private boolean targetSupportsDebugging() { + if (target instanceof DebuggableTarget) { + return ((DebuggableTarget) target).isDebuggingSupported(); + } + return false; + } + + private boolean isSupportedMode(BootDashElement element) { + return element!=null && element.supportedGoalStates().contains(runOrDebug); + } + + @Override + public void run() { + try { + final BootDashElement element = getSingleSelectedElement(); + if (element != null) { + final IProject project = element.getProject(); + if (project != null) { + BootDashModel targetModel = model.getSectionByTargetId(target.getId()); + if (targetModel instanceof ProjectDeploymentTarget) { + ((ProjectDeploymentTarget) targetModel).performDeployment(ImmutableSet.of(project), runOrDebug); + } + } + } + } catch (Exception e) { + Log.log(e); + } + } + + @Override + public void dispose() { + target.getClientExp().removeListener(connectionListener); + super.dispose(); + } + + @Override + public String toString() { + return this.getClass().getSimpleName()+"("+runOrDebug+", "+target+")"; + } + + public RemoteRunTarget getTarget() { + return target; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/DeploymentProperties.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/DeploymentProperties.java new file mode 100644 index 000000000..19d1fb0c1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/DeploymentProperties.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2016, 2018 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.deployment; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Interface for Cloud Foundry application deployment properties + * + * @author Alex Boyko + * + */ +public interface DeploymentProperties { + + int DEFAULT_MEMORY = 1024; + int DEFAULT_INSTANCES = 1; + String DEFAULT_HEALTH_CHECK_TYPE = "port"; + String DEFAULT_HEALTH_CHECK_HTTP_ENDPOINT = "/"; + + String getAppName(); + + int getMemory(); + + int getDiskQuota(); + + Integer getTimeout(); + + String getHealthCheckType(); + + String getHealthCheckHttpEndpoint(); + + String getBuildpack(); + + List getBuildpacks(); + + String getCommand(); + + String getStack(); + + Map getEnvironmentVariables(); + + int getInstances(); + + List getServices(); + + Set getUris(); + + /** + * If the origin of these properties is parsing yml content (from file or + * embedded editor buffer, then this method retrieves the raw text of the + * yml content). If the properties where obtained some other way (e.g. by reading + * app state from CF itself, it returns null). + */ + default String getYamlContent() { return null; } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/InstantForceableReconciler.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/InstantForceableReconciler.java new file mode 100644 index 000000000..7a3aba9b4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/InstantForceableReconciler.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2016 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.deployment; + +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.reconciler.AbstractReconciler; +import org.eclipse.jface.text.reconciler.DirtyRegion; +import org.eclipse.jface.text.reconciler.IReconcilingStrategy; +import org.springframework.ide.eclipse.editor.support.ForceableReconciler; + +/** + * Reconciler able to start reconciling almost instantly without waiting time + * specified by the delay parameter before processing dirty regions. Input + * document change will trigger instant reconcile. This is useful for single + * viewer used for displaying content of different files for instance, file + * input changes and reconcile happens instantly. + * + * @author Alex Boyko + * + */ +public class InstantForceableReconciler extends ForceableReconciler { + + private final static int NO_DELAY = 1; + + /** + * Original delay. Default value must match the one specified in {@link AbstractReconciler} + */ + private int fOriginalDelay = 500; + + /** + * Stores original delay value when instant reconcile is performed, 0 otherwise + */ + private int fTempDelay = NO_DELAY; + + /** + * Creates instance of the reconciler + * @param strategy Reconcile strategy + */ + public InstantForceableReconciler(IReconcilingStrategy strategy) { + super(strategy); + } + + @Override + protected void reconcilerDocumentChanged(IDocument document) { + super.reconcilerDocumentChanged(document); + // Force instant reconciling + forceReconcileNow(); + } + + @Override + public void setDelay(int delay) { + super.setDelay(delay); + fOriginalDelay = delay; + } + + public void forceReconcileNow() { + if (fOriginalDelay > NO_DELAY && fTempDelay == NO_DELAY) { + // Remember original delay value + int temp = fOriginalDelay; + // Set delay to 0 to start processing dirty regions immediately + setDelay(NO_DELAY); + // Remember the original delay value to set it back when reconciling is complete + fTempDelay = temp; + forceReconcile(); + } + } + + @Override + protected void process(DirtyRegion dirtyRegion) { + super.process(dirtyRegion); + if (fTempDelay != NO_DELAY) { + // If there is original delay stored set it back on now + setDelay(fTempDelay); + fTempDelay = NO_DELAY; + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/YamlFileInput.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/YamlFileInput.java new file mode 100644 index 000000000..3b19ee997 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/YamlFileInput.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2016 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.deployment; + +import java.io.ByteArrayInputStream; + +import org.eclipse.compare.ResourceNode; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; +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.swt.graphics.Image; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; + +/** + * Compare and Merge YAML file input + * + * @author Alex Boyko + * + */ +public class YamlFileInput extends ResourceNode { + + private Image image; + + public YamlFileInput(IFile manifestFile, Image image) { + super(manifestFile); + this.image = image; + } + + @Override + public String getType() { + return "yml"; + } + + @Override + public void setContent(final byte[] contents) { + super.setContent(contents); + Job job = new Job("Saving changes to deployment manifest file '" + getResource().getFullPath() + "'") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + ((IFile)getResource()).setContents(new ByteArrayInputStream(contents), true, true, monitor); + } catch (CoreException e) { + e.getStatus(); + } + return Status.OK_STATUS; + } + }; + job.setRule(getResource()); + job.schedule(); + } + + @Override + public Image getImage() { + if (image == null) { + return super.getImage(); + } else { + return BootDashActivator.getDefault().getImageRegistry().get(BootDashActivator.MANIFEST_ICON); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/YamlInput.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/YamlInput.java new file mode 100644 index 000000000..74cc6c5fb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/YamlInput.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2016 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.deployment; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import org.eclipse.compare.IStreamContentAccessor; +import org.eclipse.compare.ITypedElement; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.swt.graphics.Image; + +/** + * Compare and Merge input for YAML string + * + * @author Alex Boyko + * + */ +public class YamlInput implements ITypedElement, IStreamContentAccessor { + + private String name; + private Image image; + private String content; + + public YamlInput(String name, Image image, String content) { + super(); + this.name = name; + this.image = image; + this.content = content; + } + + public String getName() { + return name; + } + + public Image getImage() { + return image; + } + + public String getType() { + return "yml"; + } + + @Override + public InputStream getContents() throws CoreException { + return new ByteArrayInputStream(content.getBytes()); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/YamlMergeViewerCreator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/YamlMergeViewerCreator.java new file mode 100644 index 000000000..163c17c06 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/cloudfoundry/deployment/YamlMergeViewerCreator.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2016, 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.deployment; + +import org.eclipse.compare.CompareConfiguration; +import org.eclipse.compare.IViewerCreator; +import org.eclipse.compare.contentmergeviewer.TextMergeViewer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.internal.ui.JavaPlugin; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.text.TextViewer; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.jface.text.source.projection.ProjectionViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.internal.genericeditor.ExtensionBasedTextViewerConfiguration; +import org.eclipse.ui.part.FileEditorInput; +import org.springframework.ide.eclipse.boot.launch.properties.EmbeddedEditor; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * Create compare and merge viewer for YAML content + * + * @author Alex Boyko + * + */ +@SuppressWarnings("restriction") +public class YamlMergeViewerCreator implements IViewerCreator { + + @Override + public Viewer createViewer(Composite parent, CompareConfiguration config) { + return new TextMergeViewer(parent, SWT.NONE, config) { + + @Override + protected SourceViewer createSourceViewer(Composite parent, int textOrientation) { +// try { +// IPreferenceStore preferenceStore = JavaPlugin.getDefault().getCombinedPreferenceStore(); +// EmbeddedEditor editor = new EmbeddedEditor(e -> new ExtensionBasedTextViewerConfiguration(e, preferenceStore), preferenceStore, false, false); +// editor.init(null, new FileEditorInput((IFile) config.getProperty("manifest"))); +// editor.createControl(parent); +// ProjectionViewer viewer = editor.getViewer(); +// viewer.getControl().addDisposeListener(e -> editor.dispose()); +// return viewer; +// } catch (CoreException e) { +// Log.log(e); +// } +// return null; + return new SourceViewer(parent, null, null, true, SWT.V_SCROLL | SWT.H_SCROLL | SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION); + } + + @Override + protected void configureTextViewer(TextViewer textViewer) { + /* + * Nothing to do + */ + } + + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/ApplicationLogConsole.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/ApplicationLogConsole.java new file mode 100644 index 000000000..e80cee5fa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/ApplicationLogConsole.java @@ -0,0 +1,200 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.console; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.debug.ui.IDebugUIConstants; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleManager; +import org.eclipse.ui.console.IOConsoleOutputStream; +import org.eclipse.ui.console.MessageConsole; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import reactor.core.Disposable; + +public class ApplicationLogConsole extends MessageConsole implements IPropertyChangeListener, IApplicationLogConsole { + + private Map activeStreams = new HashMap<>(); + + private Disposable logStreamingToken; + + public ApplicationLogConsole(String name, String type, ImageDescriptor icon) { + super(name, type, icon, true); + } + + public synchronized void setLogStreamingToken(Disposable logStreamingToken) { + if (this.logStreamingToken != null) { + this.logStreamingToken.dispose(); + } + this.logStreamingToken = logStreamingToken; + } + + public synchronized Disposable getLogStreamingToken() { + return this.logStreamingToken; + } + + public synchronized void writeLog(LogMessage log) { + if (log == null) { + return; + } + final LogType logType = log.getType(); + writeApplicationLog(log.getMessage(), logType); + } + + /** + * + * @param message + * @param type + * @return true if successfully wrote to stream. False otherwise + */ + public synchronized void writeApplicationLog(String message, LogType type) { + if (message != null) { + IOConsoleOutputStream stream = getStream(type); + + try { + if (stream != null && !stream.isClosed()) { + message = format(message); + stream.write(message); + } + } catch (IOException e) { + Log.log(e); + } + } + } + + protected static String format(String message) { + if (message.contains("\n") || message.contains("\r")) { + return message; + } + return message + '\n'; + } + + /** + * Closes the console AND removes it from the console manager in Eclipse. + *

+ * IMPORTANT: When the console is removed from the console manager by close, + * it ALSO destroys the console, so it is NOT necessary + * to call destroy on the console after it is closed. + *

+ * Once a console is closed, it CANNOT be used again. + */ + public synchronized void close() { + setLogStreamingToken(null); + + for (IOConsoleOutputStream outputStream : activeStreams.values()) { + if (!outputStream.isClosed()) { + try { + outputStream.close(); + } catch (IOException e) { + Log.log(e); + } + } + } + activeStreams.clear(); + IConsoleManager manager = ConsolePlugin.getDefault().getConsoleManager(); + // IMPORTANT: by removing from the console manager, the console is ALSO automatically + // destroyed. See javadoc for org.eclipse.ui.console.AbstractConsole.destroy() + // See also PT 170262394 + manager.removeConsoles(new IConsole[] { this }); + } + + protected synchronized IOConsoleOutputStream getStream(final LogType logType) { + + IOConsoleOutputStream stream = activeStreams.get(logType); + // If the console is no longer managed by the Eclipse console manager, + // do NOT + // write to the stream to avoid exceptions + if (!isStillManaged() || (stream != null && stream.isClosed())) { + return null; + } + if (stream == null) { + stream = newOutputStream(); + + final IOConsoleOutputStream toConfig = stream; + + // Setting colour must be done in UI thread + Display.getDefault().syncExec(new Runnable() { + + @Override + public void run() { + toConfig.setColor(Display.getDefault().getSystemColor(logType.getDisplayColour())); + } + }); + + activeStreams.put(logType, stream); + } + return stream; + } + + protected synchronized boolean isStillManaged() { + IConsoleManager manager = ConsolePlugin.getDefault().getConsoleManager(); + + IConsole[] activeConsoles = manager.getConsoles(); + if (activeConsoles != null) { + for (IConsole console : activeConsoles) { + if (console.getName().equals(this.getName())) { + return true; + } + } + } + return false; + } + + + @Override + public void onMessage(LogMessage log) { + writeLog(log); + } + + @Override + public void onComplete() { + // Leave open for tail + } + + @Override + public void onError(Throwable exception) { + writeApplicationLog(exception.getMessage(), LogType.STDERROR); + } + + @Override + protected void init() { + super.init(); + JFaceResources.getFontRegistry().addListener(this); + } + + @Override + protected void dispose() { + JFaceResources.getFontRegistry().removeListener(this); + super.dispose(); + } + + /** + * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent) + */ + @Override + public void propertyChange(PropertyChangeEvent evt) { + String property = evt.getProperty(); + if (property.equals(IDebugUIConstants.PREF_CONSOLE_FONT)) { + setFont(JFaceResources.getFont(IDebugUIConstants.PREF_CONSOLE_FONT)); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/CloudAppLogManager.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/CloudAppLogManager.java new file mode 100644 index 000000000..3c5aaecf6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/CloudAppLogManager.java @@ -0,0 +1,226 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.console; + +import java.io.IOException; + +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleManager; +import org.eclipse.ui.console.MessageConsole; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.AppConsole; +import org.springframework.ide.eclipse.boot.dash.api.AppConsoleProvider; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.views.BootDashModelConsoleManager; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import reactor.core.Disposable; + +public class CloudAppLogManager extends BootDashModelConsoleManager implements AppConsoleProvider { + + static final String CONSOLE_TYPE = "org.springframework.ide.eclipse.boot.dash.console"; + static final String APP_CONSOLE_ID = "consoleId"; + + private IConsoleManager consoleManager; + + public CloudAppLogManager(SimpleDIContext injections) { + consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + } + + @Override + protected void doWriteToConsole(App element, String message, LogType type) throws Exception { + ApplicationLogConsole console = getExisitingConsole(element); + if (console != null) { + console.writeApplicationLog(message, type); + + // To avoid console "jumping", only show console + // if none are visible + showIfNoOtherConsoles(console); + } + } + + public IConsoleManager getConsoleManager() { + return consoleManager; + } + + /** + * Only shows the given console if no other console is visible. This avoid + * "jumping" between consoles when multiple consoles are streaming in + * parallel. + */ + protected void showIfNoOtherConsoles(IConsole console) { + IConsole[] consoles = consoleManager.getConsoles(); + if (consoles == null || consoles.length == 0) { + consoleManager.showConsoleView(console); + } + } + + @Override + public synchronized void resetConsole(App element) { + ApplicationLogConsole console = getExisitingConsole(element); + if (console != null) { + console.clearConsole(); + console.setLogStreamingToken(null); + } + } + + /** + * + * @param targetProperties + * @param appName + * @return existing console, or null if it does not exist. + */ + public synchronized ApplicationLogConsole getExisitingConsole(App element) { + IConsole[] consoles = ConsolePlugin.getDefault().getConsoleManager().getConsoles(); + if (consoles != null) { + for (IConsole console : consoles) { + if (console instanceof ApplicationLogConsole) { + String id = (String) ((MessageConsole) console).getAttribute(APP_CONSOLE_ID); + String idToCheck = getConsoleId(element); + if (idToCheck.equals(id)) { + ApplicationLogConsole appConsole = (ApplicationLogConsole) console; + connect(appConsole, element); + return appConsole; + } + } + } + } + + return null; + } + + @Override + public synchronized void terminateConsole(App element) throws Exception { + ApplicationLogConsole console = getExisitingConsole(element); + if (console != null) { + console.close(); + } + } + + /** + * + * @param targetProperties + * @param appName + * @return non-null console for the given appname and target properties + * @throws Exception + * if console was not created or found + */ + public synchronized ApplicationLogConsole getOrCreateConsole(App element) throws Exception { + ApplicationLogConsole appConsole = getExisitingConsole(element); + + if (appConsole == null) { + appConsole = new ApplicationLogConsole(getConsoleDisplayName(element), CONSOLE_TYPE, element.getTarget().getIcon()); + appConsole.setAttribute(APP_CONSOLE_ID, getConsoleId(element)); + consoleManager.addConsoles(new IConsole[] { appConsole }); + + connect(appConsole, element); + showConsole(element); + } + + return appConsole; + } + + + + protected void connect(ApplicationLogConsole logConsole, App element) { + if (logConsole == null) { + return; + } + if (element instanceof LegacyLogSource) { + Disposable existingToken = logConsole.getLogStreamingToken(); + if (existingToken==null) { + LegacyLogSource source = (LegacyLogSource) element; + logConsole.setLogStreamingToken(source.connectLog(logConsole)); + } + } + } + + public static String getConsoleId(App element) { + return element.getTarget().getId()+":"+element.getName(); + } + + public static String getConsoleDisplayName(App element) { + String displayName = element.getConsoleDisplayName(); + return displayName!=null ? displayName :element.getName() +" @ "+ element.getTarget().getDisplayName(); + } + + @Override + public void showConsole(App element) throws Exception { + ApplicationLogConsole console = getOrCreateConsole(element); + consoleManager.showConsoleView(console); + } + + @Override + public void reconnect(App element) throws Exception { + ApplicationLogConsole console = getOrCreateConsole(element); + console.setLogStreamingToken(null); + connect(console, element); + consoleManager.showConsoleView(console); + } + + @Override + public ApplicationLogConsole safeGetOrCreateConsole(App element) { + try { + return getOrCreateConsole(element); + } catch (Exception e) { + return null; + } + } + + public boolean hasConsole(App element) { + return getExisitingConsole(element) != null; + } + + @Override + public AppConsole getConsole(App app) { + return new AppConsole() { + + @Override + public String toString() { + return "AppConsole("+app.getName()+")"; + } + + @Override + public void write(String message, LogType type) throws Exception { + writeToConsole(app, message, type); + } + + @Override + public void show() { + try { + showConsole(app); + } catch (Exception e) { + Log.log(e); + } + } + }; + } + + public void showConsole(RunTarget target, String appName) throws Exception { + showConsole(new App() { + + @Override + public String getName() { + return appName; + } + + @Override + public RunTarget getTarget() { + return target; + } + + }); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/ConsolePageParticipant.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/ConsolePageParticipant.java new file mode 100644 index 000000000..b1bb67ef7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/ConsolePageParticipant.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2020 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.console; + +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleConstants; +import org.eclipse.ui.console.IConsoleManager; +import org.eclipse.ui.console.IConsolePageParticipant; +import org.eclipse.ui.console.IConsoleView; +import org.eclipse.ui.part.IPageBookViewPage; + +public class ConsolePageParticipant implements IConsolePageParticipant { + + private RemoveConsoleAction removeConsoleAction; + + @Override + public T getAdapter(Class adapter) { + return null; + } + + @Override + public void init(IPageBookViewPage page, IConsole console) { + IConsoleView view = (IConsoleView) page.getSite().getPage().findView(IConsoleConstants.ID_CONSOLE_VIEW); + IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + removeConsoleAction = new RemoveConsoleAction(view, consoleManager); + IActionBars actionBars = page.getSite().getActionBars(); + configureToolBar(actionBars.getToolBarManager()); + } + + private void configureToolBar(IToolBarManager toolBarManager) { + toolBarManager.appendToGroup(IConsoleConstants.LAUNCH_GROUP, removeConsoleAction); + } + + @Override + public void dispose() { + } + + @Override + public void activated() { + } + + @Override + public void deactivated() { + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/IApplicationLogConsole.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/IApplicationLogConsole.java new file mode 100644 index 000000000..7296f5431 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/IApplicationLogConsole.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2016 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.console; + +public interface IApplicationLogConsole { + + void onMessage(LogMessage log); + + void onComplete(); + + void onError(Throwable exception); + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/LegacyLogSource.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/LegacyLogSource.java new file mode 100644 index 000000000..3c47d12b0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/LegacyLogSource.java @@ -0,0 +1,7 @@ +package org.springframework.ide.eclipse.boot.dash.console; + +import reactor.core.Disposable; + +public interface LegacyLogSource { + Disposable connectLog(ApplicationLogConsole logConsole); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/LogMessage.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/LogMessage.java new file mode 100644 index 000000000..f1d14bde0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/LogMessage.java @@ -0,0 +1,26 @@ +package org.springframework.ide.eclipse.boot.dash.console; + +public class LogMessage { + + private LogType type; + private String msg; + + public LogMessage(LogType type, String msg) { + super(); + this.msg = msg; + this.type = type; + } + + public String getMessage() { + return msg; + } + + public LogType getType() { + return type; + } + + @Override + public String toString() { + return "LogMessage [type=" + type + ", msg=" + msg + "]"; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/LogType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/LogType.java new file mode 100644 index 000000000..f4e3cad73 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/LogType.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2015 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.console; + +import org.eclipse.swt.SWT; + +public class LogType { + /* + * Local messages types + */ + public static final LogType STDOUT = new LogType(SWT.COLOR_DARK_BLUE); + public static final LogType STDERROR = new LogType(SWT.COLOR_RED); + public static final LogType APP_ERROR = new LogType(SWT.COLOR_RED); + public static final LogType APP_OUT = new LogType(SWT.COLOR_DARK_GREEN); + + private final int displayColour; + + public LogType(int displayColour) { + this.displayColour = displayColour; + } + + public int getDisplayColour() { + return this.displayColour; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/RemoveConsoleAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/RemoveConsoleAction.java new file mode 100644 index 000000000..9d5182932 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/console/RemoveConsoleAction.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2020 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.console; + +import org.eclipse.debug.internal.ui.DebugPluginImages; +import org.eclipse.debug.internal.ui.IInternalDebugUIConstants; +import org.eclipse.jface.action.Action; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleManager; +import org.eclipse.ui.console.IConsoleView; + +@SuppressWarnings("restriction") +public class RemoveConsoleAction extends Action { + + private IConsoleView consoleView; + private IConsoleManager consoleManager; + + public RemoveConsoleAction(IConsoleView consoleView, IConsoleManager consoleManager) { + this.consoleView = consoleView; + this.consoleManager = consoleManager; + setDisabledImageDescriptor(DebugPluginImages.getImageDescriptor(IInternalDebugUIConstants.IMG_DLCL_REMOVE)); + setHoverImageDescriptor(DebugPluginImages.getImageDescriptor(IInternalDebugUIConstants.IMG_ELCL_REMOVE)); + setEnabled(true); + } + + @Override + public void run() { + ApplicationLogConsole applicationConsole = getApplicationConsole(); + consoleManager.removeConsoles(new IConsole[] { applicationConsole }); + } + + private ApplicationLogConsole getApplicationConsole() { + if (consoleView != null) { + IConsole console = consoleView.getConsole(); + if (console instanceof ApplicationLogConsole) { + return (ApplicationLogConsole) console; + } + } + return null; + + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/debug/DebugSelectionListener.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/debug/DebugSelectionListener.java new file mode 100644 index 000000000..a275c8aa4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/debug/DebugSelectionListener.java @@ -0,0 +1,52 @@ +//package org.springframework.ide.eclipse.boot.dash.debug; +// +//import org.eclipse.jface.text.ITextSelection; +//import org.eclipse.jface.viewers.ISelection; +//import org.eclipse.jface.viewers.IStructuredSelection; +//import org.eclipse.ui.ISelectionListener; +//import org.eclipse.ui.ISelectionService; +//import org.eclipse.ui.IWorkbenchPart; +//import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; +// +//public class DebugSelectionListener implements ISelectionListener, Disposable { +// +// private ISelectionService selectionService; +// +// public DebugSelectionListener(ISelectionService selectionService) { +// this.selectionService = selectionService; +// selectionService.addSelectionListener(this); +// } +// +// @Override +// public void selectionChanged(IWorkbenchPart part, ISelection selection) { +// showSelection(selection); +// } +// +// private void showSelection(ISelection selection) { +// if (selection instanceof IStructuredSelection) { +// showSelection((IStructuredSelection) selection); +// } else if (selection instanceof ITextSelection) { +// showSelection((ITextSelection) selection); +// } +// } +// +// private void showSelection(ITextSelection selection) { +// System.out.println(">>> TextSelection:"); +// System.out.println(selection.getText()); +// System.out.println("<<< TextSelection"); +// } +// +// private void showSelection(IStructuredSelection selection) { +// System.out.println(">>> StructuredSelection:"); +// for (Object e : selection.toArray()) { +// System.out.println(e); +// } +// System.out.println("<<< StructuredSelection"); +// } +// +// @Override +// public void dispose() { +// selectionService.removeSelectionListener(this); +// } +// +//} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/devtools/DevtoolsPortRefresher.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/devtools/DevtoolsPortRefresher.java new file mode 100644 index 000000000..1f499dbff --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/devtools/DevtoolsPortRefresher.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright (c) 2013 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.devtools; + +import org.eclipse.core.resources.IProject; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.IStreamListener; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.debug.core.model.IProcess; +import org.eclipse.debug.core.model.IStreamMonitor; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElementFactory; +import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement; +import org.springframework.ide.eclipse.boot.dash.model.LocalBootDashModel; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.util.ProcessTracker; +import org.springframework.ide.eclipse.boot.util.ProcessTracker.ProcessListener; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +/** + * Responsible for 'poking' local boot dash elements when they need to refresh port info + * after boot devtools does an in-place restart of an app. + *

+ * As devtools autonously refreshes an app without any involvement of Eclipse... the only way we + * can see that something happened to the process is by watching the process's output. + *

+ * A DevtoolsPortRefresher uses a 'ProcessTracker' to attach an output monitor to any local process + * that is being started and corresponds to a local boot dash element with devtools enabled. + * + * @author Kris De Volder + */ +public class DevtoolsPortRefresher implements Disposable, ProcessListener { + + private ProcessTracker processTracker; + private BootProjectDashElementFactory elementFactory; + + public DevtoolsPortRefresher(LocalBootDashModel localBootDashModel, BootProjectDashElementFactory elementFactory) { + processTracker = new ProcessTracker(this); + this.elementFactory = elementFactory; + } + + @Override + public void dispose() { + processTracker.dispose(); + } + + @Override + public void debugTargetCreated(ProcessTracker tracker, IDebugTarget target) { + IProcess process = target.getProcess(); + if (process!=null) { // may be null. E.g. for CF debug targets there's no local process attached to debug target. + processCreated(tracker, process); + } + } + + @Override + public void processCreated(ProcessTracker tracker, IProcess process) { + final BootProjectDashElement element = getElementFor(process); + if (element!=null) { + process.getStreamsProxy().getOutputStreamMonitor().addListener(new IStreamListener() { + public void streamAppended(String text, IStreamMonitor monitor) { + if (text.contains("started on port")) { + element.refreshLivePorts(); + } + } + }); + } + } + + /** + * Gets the element this process is related to, if the element is 'interesting'. + * @return The element or null (if there's no corresponding element or its not 'interesting') + */ + private BootProjectDashElement getElementFor(IProcess process) { + ILaunch launch = process.getLaunch(); + try { + if (launch!=null) { + ILaunchConfiguration conf = launch.getLaunchConfiguration(); + if (conf!=null && conf.getType().getIdentifier().equals(BootLaunchConfigurationDelegate.TYPE_ID)) { + IProject p = BootLaunchConfigurationDelegate.getProject(conf); + if (p!=null && BootPropertyTester.hasDevtools(p)) { + BootDashElement e = elementFactory.createOrGet(p); + if (BootPropertyTester.hasDevtools(p)) { + if (e instanceof BootProjectDashElement) { // this test should always succeed but check it anyway + return (BootProjectDashElement) e; + } + } + } + } + } + } catch (Exception e) { + BootActivator.log(e); + } + return null; + } + + @Override + public void debugTargetTerminated(ProcessTracker tracker, IDebugTarget target) { + //don't care + } + + @Override + public void processTerminated(ProcessTracker tracker, IProcess process) { + //don't care + } + + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/devtools/DevtoolsUtil.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/devtools/DevtoolsUtil.java new file mode 100644 index 000000000..956274772 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/devtools/DevtoolsUtil.java @@ -0,0 +1,398 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.devtools; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.debug.core.DebugException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.debug.core.model.IProcess; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.DevtoolsConnectable; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElement; +import org.springframework.ide.eclipse.boot.dash.util.CollectionUtils; +import org.springframework.ide.eclipse.boot.dash.views.RestartDevtoolsClientAction; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.devtools.BootDevtoolsClientLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.util.ProcessListenerAdapter; +import org.springframework.ide.eclipse.boot.util.ProcessTracker; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; + +/** + * @author Kris De Volder + */ +public class DevtoolsUtil { + + private static final String TARGET_ID = "boot.dash.target.id"; + private static final String APP_NAME = "boot.dash.cloudfoundry.app-name"; + + private static final QualifiedName REMOTE_CLIENT_SECRET_PROPERTY = new QualifiedName(BootDashActivator.PLUGIN_ID, "spring.devtools.remote.secret"); + + private static final String JAVA_OPTS_ENV_VAR = "JAVA_OPTS"; + public static final String REMOTE_SECRET_PROP = "spring.devtools.remote.secret"; + private static final String REMOTE_SECRET_JVM_ARG = "-D"+REMOTE_SECRET_PROP+"="; +// private static final String REMOTE_DEBUG_JVM_ARGS = "-Dspring.devtools.restart.enabled=false -Xdebug -Xrunjdwp:server=y,transport=dt_socket,suspend=n"; + + private static ILaunchManager getLaunchManager() { + return DebugPlugin.getDefault().getLaunchManager(); + } + + private static ILaunchConfigurationType getConfigurationType() { + return getLaunchManager().getLaunchConfigurationType(BootDevtoolsClientLaunchConfigurationDelegate.TYPE_ID); + } + + private static ILaunchConfigurationWorkingCopy createConfiguration(IProject project, BootDashElement bde) throws CoreException { + ILaunchConfigurationType configType = getConfigurationType(); + String projectName = project.getName(); + ILaunchConfigurationWorkingCopy wc = configType.newInstance(null, getLaunchManager().generateLaunchConfigurationName("remote-devtools-client["+projectName+"]")); + + BootLaunchConfigurationDelegate.setProject(wc, project); + BootDevtoolsClientLaunchConfigurationDelegate.setRemoteUrl(wc, remoteUrl(bde)); + BootDevtoolsClientLaunchConfigurationDelegate.setManaged(wc, true); + + wc.setMappedResources(new IResource[] {project}); + return wc; + } + + public static String remoteUrl(BootDashElement bde) { + String host = bde.getLiveHost(); + Integer port = CollectionUtils.getSingle(bde.getLivePorts()); + String proto = bde.getProtocol(); + if (proto!=null && host!=null && port!=null && port>0) { + return proto +"://"+host+":"+port; + } + return null; + } + + public static ILaunch launchDevtools(IProject project, String debugSecret, BootDashElement bde, String mode, IProgressMonitor monitor) throws CoreException { + ILaunchConfiguration conf = getOrCreateLaunchConfig(project, debugSecret, bde); + if (conf!=null) { + return conf.launch(mode, monitor == null ? new NullProgressMonitor() : monitor); + } + throw ExceptionUtil.coreException("Can't launch, no remote url?"); + } + + private static ILaunchConfiguration getOrCreateLaunchConfig(IProject project, String debugSecret, BootDashElement bde) throws CoreException { + String remoteUrl = DevtoolsUtil.remoteUrl(bde); + if (remoteUrl!=null) { + ILaunchConfiguration existing = findConfig(project, remoteUrl); + ILaunchConfigurationWorkingCopy wc; + if (existing!=null) { + wc = existing.getWorkingCopy(); + } else { + wc = createConfiguration(project, bde); + } + BootDevtoolsClientLaunchConfigurationDelegate.setRemoteSecret(wc, debugSecret); + setElement(wc, bde); + return wc.doSave(); + } + return null; + } + + private static ILaunchConfiguration findConfig(IProject project, String remoteUrl) { + try { + for (ILaunchConfiguration c : getLaunchManager().getLaunchConfigurations(getConfigurationType())) { + if (project.equals(BootLaunchConfigurationDelegate.getProject(c)) + && remoteUrl.equals(BootDevtoolsClientLaunchConfigurationDelegate.getRemoteUrl(c))) { + return c; + } + } + } catch (CoreException e) { + Log.log(e); + } + return null; + } + + private static List findLaunches(IProject project, BootDashElement bde) { + String remoteUrl = remoteUrl(bde); + if (remoteUrl!=null) { + List launches = new ArrayList<>(); + for (ILaunch l : getLaunchManager().getLaunches()) { + try { + ILaunchConfiguration c = l.getLaunchConfiguration(); + if (c!=null) { + if (project.equals(BootLaunchConfigurationDelegate.getProject(c)) + && remoteUrl.equals(BootDevtoolsClientLaunchConfigurationDelegate.getRemoteUrl(c))) { + launches.add(l); + } + } + } catch (Exception e) { + Log.log(e); + } + } + return launches; + } + return ImmutableList.of(); + } + + + public static boolean isDevClientAttached(BootDashElement bde, String launchMode) { + IProject project = bde.getProject(); + if (project!=null) { // else not associated with a local project... can't really attach debugger then + String host = bde.getLiveHost(); + if (host!=null) { // else app not running, can't attach debugger then + return isLaunchMode(findLaunches(project, bde), launchMode); + } + } + return false; + } + + private static boolean isLaunchMode(List launches, String launchMode) { + for (ILaunch l : launches) { + if (!l.isTerminated()) { + if (ILaunchManager.DEBUG_MODE.equals(launchMode) && launchMode.equals(l.getLaunchMode())) { + for (IDebugTarget p : l.getDebugTargets()) { + if (!p.isDisconnected() && !p.isTerminated()) { + return true; + } + } + } else if (ILaunchManager.RUN_MODE.equals(launchMode) && launchMode.equals(l.getLaunchMode())) { + for (IProcess p : l.getProcesses()) { + if (!p.isTerminated()) { + return true; + } + } + } else if (launchMode == null) { + // Launch mode not specified? Launch is not terminated hence just return true + return true; + } + } + } + return false; + } + + public static ILaunch launchDevtools(BootDashElement cde, String debugSecret, String mode, IProgressMonitor monitor) throws CoreException { + return launchDevtools(cde.getProject(), debugSecret, cde, mode, monitor); + } + + public static void setElement(ILaunchConfigurationWorkingCopy l, BootDashElement bde) { + //Tag the launch so we can easily determine what CDE it belongs to later. + l.setAttribute(TARGET_ID, bde.getTarget().getId()); + l.setAttribute(APP_NAME, bde.getName()); + } + + public static boolean isLaunchFor(ILaunch l, BootDashElement bde) { + String targetId = getAttribute(l, TARGET_ID); + String appName = getAttribute(l, APP_NAME); + if (targetId!=null && appName!=null) { + return targetId.equals(bde.getTarget().getId()) + && appName.equals(bde.getName()); + } + return false; + } + + /** + * Retreive corresponding CDE for a given launch. + */ + public static BootDashElement getElement(ILaunchConfiguration l, BootDashViewModel model) { + String targetId = getAttribute(l, TARGET_ID); + String appName = getAttribute(l, APP_NAME); + if (targetId!=null && appName!=null) { + BootDashModel section = model.getSectionByTargetId(targetId); + if (section!=null) { + return section.getApplication(appName); + } + } + return null; + } + + public static BootDashElement getElement(ILaunch l, BootDashViewModel viewModel) { + ILaunchConfiguration conf = l.getLaunchConfiguration(); + if (conf!=null) { + return getElement(conf, viewModel); + } + return null; + } + + + private static String getAttribute(ILaunch l, String name) { + try { + ILaunchConfiguration c = l.getLaunchConfiguration(); + if (c!=null) { + return c.getAttribute(name, (String)null); + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + private static String getAttribute(ILaunchConfiguration l, String name) { + try { + return l.getAttribute(name, (String)null); + } catch (DebugException e) { + //ignore Eclipse throws this sometimes (trying to read attribute from launch who's config was deleted. + } catch (CoreException e) { + Log.log(e); + } + return null; + } + + public static ProcessTracker createProcessTracker(final BootDashViewModel viewModel) { + return new ProcessTracker(new ProcessListenerAdapter() { + @Override + public void debugTargetCreated(ProcessTracker tracker, IDebugTarget target) { + handleStateChange(target.getLaunch(), "debugTargetCreated"); + } + @Override + public void debugTargetTerminated(ProcessTracker tracker, IDebugTarget target) { + handleStateChange(target.getLaunch(), "debugTargetTerminated"); + deleteConf(target.getLaunch()); + } + + @Override + public void processTerminated(ProcessTracker tracker, IProcess process) { + handleStateChange(process.getLaunch(), "processTerminated"); + deleteConf(process.getLaunch()); + } + private void deleteConf(ILaunch launch) { + try { + ILaunchConfiguration conf = launch.getLaunchConfiguration(); + if (conf!=null && BootDevtoolsClientLaunchConfigurationDelegate.isManaged(conf)) { + conf.delete(); + } + } catch (Exception e) { + Log.log(e); + } + } + @Override + public void processCreated(ProcessTracker tracker, IProcess process) { + handleStateChange(process.getLaunch(), "processCreated"); + } + private void handleStateChange(ILaunch l, Object info) { + BootDashElement e = DevtoolsUtil.getElement(l, viewModel); + if (e!=null) { + BootDashModel model = e.getBootDashModel(); + model.notifyElementChanged(e, info); + } + } + + @Override + public void dispose() { + try { + for (ILaunchConfiguration c : DebugPlugin.getDefault().getLaunchManager().getLaunchConfigurations()) { + if (BootDevtoolsClientLaunchConfigurationDelegate.isManaged(c)) { + c.delete(); + } + } + } catch (Exception e) { + Log.log(e); + } + } + }); + } + + public static void disconnectDevtoolsClientsFor(BootDashElement e) { + ILaunchManager lm = getLaunchManager(); + for (ILaunch l : lm.getLaunches()) { + if (!l.isTerminated() && isLaunchFor(l, e)) { + if (l.canTerminate()) { + try { + l.terminate(); + } catch (DebugException de) { + Log.log(de); + } + } + } + } + } + + public static String getSecret(IProject project) { + try { + String secret = project.getPersistentProperty(REMOTE_CLIENT_SECRET_PROPERTY); + if (secret == null) { + secret = RandomStringUtils.randomAlphabetic(20); + project.setPersistentProperty(REMOTE_CLIENT_SECRET_PROPERTY, secret); + } + return secret; + } catch (Exception e) { + Log.log(e); + return null; + } + } + + public static boolean isEnvVarSetupForRemoteClient(Map envVars, String secret) { + String javaOpts = envVars.get(JAVA_OPTS_ENV_VAR); + if (javaOpts!=null && javaOpts.matches("(.*\\s+|^)" + REMOTE_SECRET_JVM_ARG + secret + "(\\s+.*|$)")) { +// if (runOrDebug == RunState.DEBUGGING) { +// return javaOpts.matches("(.*\\s+|^)" + REMOTE_DEBUG_JVM_ARGS + "(\\s+.*|$)"); +// } else { +// return !javaOpts.matches("(.*\\s+|^)" + REMOTE_DEBUG_JVM_ARGS + "(\\s+.*|$)"); +// } + return true; + } + return false; + } + + public static void setupEnvVarsForRemoteClient(Map envVars, String secret) { + String javaOpts = clearJavaOpts(envVars.get(JAVA_OPTS_ENV_VAR)); + StringBuilder sb = javaOpts == null ? new StringBuilder() : new StringBuilder(javaOpts); + if (sb.length() > 0) { + sb.append(' '); + } + sb.append(REMOTE_SECRET_JVM_ARG); + sb.append(secret); +// if (runOrDebug == RunState.DEBUGGING) { +// sb.append(' '); +// sb.append(REMOTE_DEBUG_JVM_ARGS); +// } + envVars.put(JAVA_OPTS_ENV_VAR, sb.toString()); + } + + private static String clearJavaOpts(String opts) { + if (opts!=null) { +// opts = opts.replaceAll(REMOTE_DEBUG_JVM_ARGS + "\\s*", ""); + opts = opts.replaceAll(REMOTE_SECRET_JVM_ARG +"\\w+\\s*", ""); + } + return opts; + } + + public static void launchClientIfNeeded(GenericRemoteAppElement app) { + if (wantsDevtools(app)) { + if (!isDevClientAttached(app, ILaunchManager.RUN_MODE)) { + app.restartRemoteDevtoolsClient(); + } + } + } + + private static boolean wantsDevtools(GenericRemoteAppElement app) { + if (app.getRunState().isActive()) { + App data = app.getAppData(); + return data instanceof DevtoolsConnectable && ((DevtoolsConnectable)data).isDevtoolsConnectable().isTrue(); + } + return false; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/di/EclipseBeanLoader.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/di/EclipseBeanLoader.java new file mode 100644 index 000000000..4faaa7208 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/di/EclipseBeanLoader.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2020 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.di; + +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.Platform; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public class EclipseBeanLoader { + + public interface Contribution { + void applyBeanDefinitions(SimpleDIContext context) throws Exception; + } + + final private SimpleDIContext context; + + public EclipseBeanLoader(SimpleDIContext context) { + this.context = context; + } + + public void loadFromExtensionPoint(String extensionPointId) { + for (IConfigurationElement ce : Platform.getExtensionRegistry().getConfigurationElementsFor(extensionPointId)) { + try { + Contribution contribution = (Contribution) ce.createExecutableExtension("class"); + contribution.applyBeanDefinitions(context); + } catch (Exception e) { + Log.log(e); + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/di/SimpleDIContext.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/di/SimpleDIContext.java new file mode 100644 index 000000000..a06b5ce65 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/di/SimpleDIContext.java @@ -0,0 +1,203 @@ +/******************************************************************************* + * Copyright (c) 2020 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.di; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.Assert; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; + +/** + * A very simple DI framework. + *

+ * Can we use something else? Maybe guice, spring, or the DI framework built-into Eclipse 4? + */ +public class SimpleDIContext { + + public static final SimpleDIContext EMPTY = new SimpleDIContext().lockdown(); + + private String creationLocation; + + public SimpleDIContext() { + this.creationLocation = ExceptionUtil.stacktrace(); + } + + private Cache, Object> beanCache = CacheBuilder.newBuilder().build(); + private Cache, List> beanListCache = CacheBuilder.newBuilder().build(); + + public interface BeanFactory { + T create(SimpleDIContext context) throws Exception; + } + + public static final class Definition { + private final Class type; + private final BeanFactory factory; + AtomicBoolean requested = new AtomicBoolean(false); + private CompletableFuture instance = new CompletableFuture<>(); + public Definition(Class type, BeanFactory factory) { + super(); + this.type = type; + this.factory = factory; + this.reload(); + } + public boolean satisfies(Class requested) { + return requested.isAssignableFrom(type); + } + public synchronized T get(SimpleDIContext context) throws Exception { + if (requested.compareAndSet(false, true)) { + try { + instance.complete(factory.create(context)); + } catch (Throwable e) { + instance.completeExceptionally(e); + } + } + return instance.get(); + } + public void reload() { + instance = new CompletableFuture<>(); + requested.set(false); + } + public CompletableFuture whenCreated(Consumer requestor) { + return instance.thenAccept(requestor); + } + @Override + public String toString() { + return "Definition("+type.getName()+")"; + } + } + + private List> definitions = new ArrayList<>(); + private AtomicBoolean locked = new AtomicBoolean(); + + public SimpleDIContext def(Class type, BeanFactory factory) { + definitions.add(new Definition<>(type, factory)); + return this; + } + + public SimpleDIContext defInstance(Class klass, T instance) { + Assert.isNotNull(instance); + def(klass, (x) -> instance); + return this; + } + + @SuppressWarnings("unchecked") + public synchronized T getBean(Class type) { + lockdown(); + try { + return (T) beanCache.get(type, () -> resolveDefinition(type).get(this)); + } catch (Exception e) { + throw ExceptionUtil.unchecked(e); + } + + } + + @SuppressWarnings("unchecked") + protected Definition resolveDefinition(Class type) { + for (int i = definitions.size()-1; i>=0; i--) { + Definition d = definitions.get(i); + if (d.satisfies(type)) { + return (Definition) d; + } + } + throw new IllegalStateException("No definition for bean of type "+type+"\n context created:\n"+creationLocation); + } + + + /** + * Prevents additional definitions from being added. The idea is that using an injection + * context proceeds in two separate stages. Stage 1 initializes the context with bean definitions. + * Stage 2 allows a client to request beans that are then created on demand. Once stage 2 is + * started, which happens automatically when the first bean is requested, the context becomes + * immmutable and no longer allows adding definitions. + */ + SimpleDIContext lockdown() { + if (locked.compareAndSet(false, true)) { + definitions = ImmutableList.copyOf(definitions); + } + return this; + } + + public void assertDefinitionFor(Class requested) { + Assert.isLegal(hasDefinitionFor(requested), "No definition for "+requested); + } + + public void assertNoDefinitionFor(Class requested) { + Assert.isLegal(!hasDefinitionFor(requested), "Definition already exists for "+requested); + } + + + public boolean hasDefinitionFor(Class requested) { + for (Definition d : definitions) { + if (d.satisfies(requested)) { + return true; + } + } + return false; + } + + @SuppressWarnings("unchecked") + public List getBeans(Class type) { + lockdown(); + try { + return (List) beanListCache.get(type, () -> { + ImmutableList.Builder builder = ImmutableList.builder(); + resolveDefinitions(type).forEach(d -> { + try { + builder.add(d.get(this)); + } catch (Exception e) { + throw ExceptionUtil.unchecked(e); + } + }); + return builder.build(); + }); + } catch (Exception e) { + throw ExceptionUtil.unchecked(e); + } + } + + @SuppressWarnings("unchecked") + private Stream> resolveDefinitions(Class type) { + Object defs = definitions.stream().filter(d -> d.satisfies(type)); + return (Stream>)defs; + } + + /** + * Clears out all bean caches and forces new beans to be created + * when they are requested again, + */ + public void reload() { + beanCache.invalidateAll(); + beanListCache.invalidateAll(); + for (Definition d : definitions) { + d.reload(); + } + } + + public Supplier supplier(Class type) { + return () -> getBean(type); + } + + public CompletableFuture whenCreated(Class type, Consumer requestor) { + return resolveDefinition(type).whenCreated(requestor); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/EditTemplateDialog.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/EditTemplateDialog.java new file mode 100644 index 000000000..e24098a87 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/EditTemplateDialog.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2016 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.dialogs; + +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.swt.widgets.Shell; +import org.springsource.ide.eclipse.commons.livexp.ui.ButtonSection; +import org.springsource.ide.eclipse.commons.livexp.ui.CheckboxSection; +import org.springsource.ide.eclipse.commons.livexp.ui.DialogWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.StringFieldSection; +import org.springsource.ide.eclipse.commons.livexp.ui.WizardPageSection; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +public class EditTemplateDialog extends DialogWithSections { + + private EditTemplateDialogModel model; + + public EditTemplateDialog(EditTemplateDialogModel model, Shell shell) { + super(model.getTitle(), model, shell); + this.model = model; + } + + @Override + protected List createSections() throws CoreException { + Builder sections = ImmutableList.builder(); + sections.add(new StringFieldSection(this, model.template).tooltip(model.getHelpText())); + sections.add(new CheckboxSection(this, model.applyToAll)); + sections.add(new ButtonSection(this, "Restore Defaults", model.restoreDefaultsHandler)); + return sections.build(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/EditTemplateDialogModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/EditTemplateDialogModel.java new file mode 100644 index 000000000..c4f15dc38 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/EditTemplateDialogModel.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2016 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.dialogs; + +import java.util.concurrent.Callable; + +import org.springsource.ide.eclipse.commons.livexp.core.BooleanFieldModel; +import org.springsource.ide.eclipse.commons.livexp.core.FieldModel; +import org.springsource.ide.eclipse.commons.livexp.core.StringFieldModel; +import org.springsource.ide.eclipse.commons.livexp.ui.OkButtonHandler; + +/** + * @author Kris De Volder + */ +public abstract class EditTemplateDialogModel implements OkButtonHandler { + + public final StringFieldModel template = new StringFieldModel("Template", getDefaultValue()); + public final FieldModel applyToAll = new BooleanFieldModel(getApplyToAllLabel(), getApplyToAllDefault()); + + + //TODO: This field can be a method once we adopt Java 8. + public Callable restoreDefaultsHandler = new Callable() { + public Void call() throws Exception { + template.getVariable().setValue(getDefaultValue()); + return null; + } + }; + + public abstract String getTitle(); + public abstract String getDefaultValue(); + public abstract String getHelpText(); + + public abstract String getApplyToAllLabel(); + public abstract boolean getApplyToAllDefault(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/ManifestDiffDialogModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/ManifestDiffDialogModel.java new file mode 100644 index 000000000..c8d37020b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/ManifestDiffDialogModel.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2016 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.dialogs; + +import org.eclipse.compare.CompareEditorInput; + +/** + * @author Kris De Volder + * @author Alex Boyko + */ +public class ManifestDiffDialogModel { + + private CompareEditorInput input; + + public ManifestDiffDialogModel(CompareEditorInput input) { + this.input = input; + } + + public enum Result { + CANCELED, + USE_MANIFEST, + FORGET_MANIFEST + } + + public CompareEditorInput getInput() { + return input; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/SelectRemoteEurekaDialog.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/SelectRemoteEurekaDialog.java new file mode 100644 index 000000000..b12a538b8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/SelectRemoteEurekaDialog.java @@ -0,0 +1,214 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.dialogs; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.ui.dialogs.SelectionStatusDialog; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashTreeLabelProvider; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashUnifiedTreeSection.BootModelViewerSorter; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +/** + * @author Martin Lippert + */ +public class SelectRemoteEurekaDialog extends SelectionStatusDialog { + + private TreeViewer tv; + private Stylers stylers; + private ITreeContentProvider contentProvider; + private Text manualEurekaURL; + private Button enterManuallyButton; + private Button selectRemoteButton; + private BootDashViewModel model; + private String choosenURL; + + public SelectRemoteEurekaDialog(Shell parent, ITreeContentProvider contentProvider) { + super(parent); + this.contentProvider = contentProvider; + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite composite = (Composite) super.createDialogArea(parent); + + createMessageArea(composite); + + selectRemoteButton = new Button(composite, SWT.RADIO); + selectRemoteButton.setText("select remote Eureka instance"); + selectRemoteButton.setSelection(true); + + tv = new TreeViewer(composite, SWT.V_SCROLL | SWT.H_SCROLL | SWT.MULTI); + tv.setSorter(new BootModelViewerSorter(this.model)); + tv.getTree().setLinesVisible(true); + stylers = new Stylers(tv.getTree().getFont()); + tv.setLabelProvider(new BootDashTreeLabelProvider(stylers, tv)); + tv.setContentProvider(contentProvider); + tv.setInput(model); + + GridData data = new GridData(GridData.FILL_BOTH); + data.widthHint = convertWidthInCharsToPixels(82); + data.heightHint = convertHeightInCharsToPixels(18); + + Tree treeWidget = tv.getTree(); + treeWidget.setLayoutData(data); + treeWidget.setFont(parent.getFont()); + + enterManuallyButton = new Button(composite, SWT.RADIO); + enterManuallyButton.setText("use this remote Eureka URL"); + + Label manualEurekaLabel = new Label(composite, SWT.NONE); + manualEurekaLabel.setText("Eureka URL:"); + manualEurekaLabel.setEnabled(false); + + manualEurekaURL = new Text(composite, SWT.SINGLE | SWT.BORDER); + manualEurekaURL.setEnabled(false); + + GridData gridData = new GridData(GridData.HORIZONTAL_ALIGN_FILL); + gridData.horizontalSpan = 3; + manualEurekaURL.setLayoutData(gridData); + + SelectionListener radioGroupListener = new SelectionListener() { + @Override + public void widgetSelected(SelectionEvent e) { + updateEnabling(); + updateOKStatus(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + }; + selectRemoteButton.addSelectionListener(radioGroupListener); + enterManuallyButton.addSelectionListener(radioGroupListener); + + tv.addSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + String url = getRemoteURL(event.getSelection()); + manualEurekaURL.setText(url); + updateOKStatus(); + } + }); + + manualEurekaURL.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + updateOKStatus(); + } + }); + + updateOKStatus(); + return composite; + } + + protected String getRemoteURL(ISelection selection) { + if (selection instanceof TreeSelection) { + Object selectedElement = ((TreeSelection) selection).getFirstElement(); + if (selectedElement instanceof BootDashElement) { + String host = ((BootDashElement) selectedElement).getLiveHost(); + if (host != null && host.length() > 0) { + if (!host.startsWith("http")) { + host = "http://" + host; + } + return host; + } + } + } + return ""; + } + + protected void updateEnabling() { + boolean remoteSelected = selectRemoteButton.getSelection(); + tv.getTree().setEnabled(remoteSelected); + manualEurekaURL.setEnabled(!remoteSelected); + } + + @Override + protected void computeResult() { + this.choosenURL = this.manualEurekaURL.getText(); + } + + protected void updateOKStatus() { + IStatus status = null; + + if (selectRemoteButton.getSelection()) { + ISelection selection = tv.getSelection(); + if (selection != null && !selection.isEmpty() && manualEurekaURL.getText() != null && manualEurekaURL.getText().length() > 0) { + status = new Status(IStatus.OK, BootDashActivator.PLUGIN_ID, null); + } + else { + status = new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, "please select a remote Eureka instance"); + } + } + else { + String manualURL = manualEurekaURL.getText(); + if (manualURL != null && manualURL.length() > 0) { + try { + URL url = new URL(manualURL); + URI uri = url.toURI(); + if (uri.getHost() == null || uri.getHost().length() == 0) { + status = new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, "please enter a valid URL"); + } + else { + status = new Status(IStatus.OK, BootDashActivator.PLUGIN_ID, null); + } + } catch (URISyntaxException e) { + status = new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, "please enter a valid URL"); + } catch (MalformedURLException e) { + status = new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, "please enter a valid URL"); + } + + } + else { + status = new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, "please enter URL of remote Eureka instance"); + } + } + + updateStatus(status); + } + + public void setInput(BootDashViewModel model) { + this.model = model; + } + + public String getSelectedEurekaURL() { + return this.choosenURL; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/ToggleFiltersDialog.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/ToggleFiltersDialog.java new file mode 100644 index 000000000..34ba8b9db --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/ToggleFiltersDialog.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2015 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.dialogs; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.swt.widgets.Shell; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel.FilterChoice; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; +import org.springsource.ide.eclipse.commons.livexp.ui.ChooseMultipleSection; +import org.springsource.ide.eclipse.commons.livexp.ui.DialogWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.WizardPageSection; + +public class ToggleFiltersDialog extends DialogWithSections { + + private ToggleFiltersDialogModel model; + + public ToggleFiltersDialog(String title, ToggleFiltersDialogModel model, Shell shell) { + super(title, model, shell); + this.model = model; + // TODO Auto-generated constructor stub + } + + public static void open(ToggleFiltersDialogModel model, Shell shell) { + ToggleFiltersDialog dlg = new ToggleFiltersDialog("Customize Filters", model, shell); + dlg.open(); + } + + @Override + protected List createSections() throws CoreException { + ChooseMultipleSection chooseFilters = + new ChooseMultipleSection(this, + "Filters", + model.getAvailableFilters(), + model.getSelectedFilters(), + Validator.OK); + return Arrays.asList((WizardPageSection)chooseFilters); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/ToggleFiltersDialogModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/ToggleFiltersDialogModel.java new file mode 100644 index 000000000..3935372ab --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/dialogs/ToggleFiltersDialogModel.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2015 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.dialogs; + +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel.FilterChoice; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSet; +import org.springsource.ide.eclipse.commons.livexp.ui.OkButtonHandler; + +/** + * Model for the ToggleFiltersDialog which allows the user to make a selection + * from a fixed-set of filters that can be toggled on/off by the user. + * + * @author Kris De Volder + */ +public class ToggleFiltersDialogModel implements OkButtonHandler { + + /** + * Filters in the view (i.e. the ones that are 'active'). + */ + private ToggleFiltersModel viewModel; + + /** + * Filters in the dialog (these get copied to the view when user pressed 'ok'). + */ + private LiveSet selectedFilters = new LiveSet(); + + public ToggleFiltersDialogModel(ToggleFiltersModel viewModel) { + this.viewModel = viewModel; + selectedFilters.replaceAll(viewModel.getSelectedFilters().getValue()); + } + + public FilterChoice[] getAvailableFilters() { + return viewModel.getAvailableFilters(); + } + + public LiveSet getSelectedFilters() { + return selectedFilters; + } + + @Override + public void performOk() throws Exception { + viewModel.getSelectedFilters().replaceAll(selectedFilters.getValue()); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/labels/BootDashLabels.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/labels/BootDashLabels.java new file mode 100644 index 000000000..950d2e3ab --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/labels/BootDashLabels.java @@ -0,0 +1,580 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.labels; + +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.*; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.resources.IProject; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.internal.ui.viewsupport.AppearanceAwareLabelProvider; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.jface.viewers.StyledString.Styler; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.PlatformUI; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.DevtoolsConnectable; +import org.springframework.ide.eclipse.boot.dash.api.Styleable; +//import org.springframework.ide.eclipse.boot.dash.cloudfoundry.CloudServiceInstanceDashElement; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.AbstractLaunchConfigurationsDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.ButtonModel; +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.TagUtils; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElement; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKClient; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKLaunchTracker; +import org.springframework.ide.eclipse.boot.dash.views.ImageDecorator; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.frameworks.core.util.StringUtils; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; + +/** + * Provides various methods for implementing various Label providers for the Boot Dash + * and its related views, dialogs etc. + *

+ * This is meant to be used as a 'delegate' object that different label provider + * implementations can wrap and use rather than a direct implementation of + * a particular label provider interface. + *

+ * Instances of this class may allocate resources (e.g. images and fonts) + * and must be disposed when they are not needed anymore. + * + * @author Alex Boyko + * @author Kris De Volder + */ +@SuppressWarnings("restriction") +public class BootDashLabels implements Disposable { + + public static abstract class Contribution { + public final ImmutableSet appliesTo; + + public Contribution(ImmutableSet appliesTo) { + super(); + this.appliesTo = appliesTo; + } + + public abstract StyledString getStyledText(Stylers stylers, BootDashElement element, BootDashColumn col); + } + + public static final String TEXT_DECORATION_COLOR_THEME = "org.springframework.ide.eclipse.boot.dash.TextDecorColor"; + public static final String ALT_TEXT_DECORATION_COLOR_THEME = "org.springframework.ide.eclipse.boot.dash.AltTextDecorColor"; + public static final String MUTED_TEXT_DECORATION_COLOR_THEME = "org.springframework.ide.eclipse.boot.dash.MutedTextDecorColor"; + + public static final char ELLIPSIS = '\u2026'; + + public static Color colorGreen() { + return PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getColorRegistry().get(TEXT_DECORATION_COLOR_THEME); + } + + public static Color colorGrey() { + return PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getColorRegistry().get(MUTED_TEXT_DECORATION_COLOR_THEME); + } + + private static final String UNKNOWN_LABEL = "???"; + + private static final Image[] NO_IMAGES = null; + + private AppearanceAwareLabelProvider javaLabels = null; + private RunStateImages runStateImages = null; + + + /** + * TODO replace 'runStateImages' and this registry with a single registry + * for working with both animatons & simple images. + */ + private ImageDecorator imageDecorator = new ImageDecorator(); + + private Stylers stylers; + + private boolean declutter = true; + final private SimpleDIContext injections; + + /** + * This constructor is deprecated. It produces something incapable of + * properly styling some kinds of labels (e.g. those requiring the use + * of a 'bold' font. Use the alternate constructor which + * takes a {@link Stylers} argument. + */ + @Deprecated + public BootDashLabels() { + //Create slighly less-capable 'Stylers': + this(SimpleDIContext.EMPTY, new Stylers(null)); + } + + public BootDashLabels(SimpleDIContext injections, Stylers stylers) { + this.injections = injections; + this.stylers = stylers; + } + + /////////////////////////////////////////////////////////////// + //// Main apis that clients should use: + + @Override + public void dispose() { + if (javaLabels != null) { + javaLabels.dispose(); + } + if (runStateImages!=null) { + runStateImages.dispose(); + runStateImages = null; + } + if (imageDecorator!=null) { + imageDecorator.dispose(); + imageDecorator = null; + } + } + + public Image[] getImageAnimation(Object e, BootDashColumn forColum) { + if (e instanceof BootDashElement) { + return getImageAnimation((BootDashElement)e, forColum); + } else if (e instanceof BootDashModel) { + return getImageAnimation((BootDashModel)e, forColum); + } + return NO_IMAGES; + } + + /** + * For those who don't care about animations, fetches the first image + * of the animation sequence only; or if the icon is non-animated just + * the image. + */ + public final Image getImage(Object element, BootDashColumn column) { + Image[] imgs = getImageAnimation(element, column); + if (imgs!=null && imgs.length>0) { + return imgs[0]; + } + return null; + } + + public StyledString getStyledText(Object element, BootDashColumn column) { + if (element instanceof BootDashElement) { + return getStyledText((BootDashElement)element, column); + } else if (element instanceof BootDashModel) { + return getStyledText((BootDashModel)element, column); + } else if (element instanceof ButtonModel) { + return getStyledText((ButtonModel)element); + } + return new StyledString(""+element); + } + + /////////////////////////////////////////////////// + // Type-specific apis below + // + // Some label providers may be only for specific types of elements and can use these + // methods instead. + + public Image[] getImageAnimation(BootDashModel element, BootDashColumn column) { + ImageDescriptor icon = getIcon(element); + ImageDescriptor decoration = getDecoration(element); + return toAnimation(icon, decoration); + } + + private ImageDescriptor getIcon(BootDashModel element) { + return element.getRunTarget().getIcon(); + } + + private ImageDescriptor getDecoration(BootDashModel element) { + if (element.getRefreshState().isError()) { + return BootDashActivator.getImageDescriptor("icons/error_ovr.gif"); + } else if (element.getRefreshState().isWarning()) { + return BootDashActivator.getImageDescriptor("icons/warning_ovr.png"); + } else if (element.getRefreshState().isLoading()) { + return BootDashActivator.getImageDescriptor("icons/waiting_ovr.gif"); + } + return null; + } + + private ImageDescriptor getDecoration(BootDashElement element) { + RefreshState refreshState = element.getRefreshState(); + if (refreshState != null) { + if (refreshState.isError()) { + return BootDashActivator.getImageDescriptor("icons/error_ovr.gif"); + } else if (refreshState.isWarning()) { + return BootDashActivator.getImageDescriptor("icons/warning_ovr.png"); + } else if (refreshState.isLoading()) { + return BootDashActivator.getImageDescriptor("icons/waiting_ovr.gif"); + } + } + return element.getRunStateImageDecoration(); + } + + private Image[] toAnimation(ImageDescriptor icon, ImageDescriptor decoration) { + Image img = imageDecorator.get(icon, decoration); + return toAnimation(img); + } + + private Image[] toAnimation(Image img) { + if (img!=null) { + return new Image[]{img}; + } + return NO_IMAGES; + } + + public Image[] getImageAnimation(BootDashElement element, BootDashColumn column) { + if (column == BootDashColumn.RUN_STATE_ICN || column == BootDashColumn.TREE_VIEWER_MAIN) { + RefreshState rs = element.getRefreshState(); + if (rs!=null) { + if (rs.isLoading()) { + return getRunStateAnimation(RunState.STARTING); + } + } + ImageDescriptor img = element.getCustomRunStateIcon(); + Image[] anim; + if (img!=null) { + anim = toAnimation(img, null); + } else { + anim = getRunStateAnimation(element.getRunState()); + } + ImageDescriptor decoration = getDecoration(element); + return imageDecorator.decorateImages(anim, decoration); + } else if (column==PROJECT) { + try { + if (element != null) { + IJavaProject jp = element.getJavaProject(); + return jp == null ? new Image[0] : new Image[] { getJavaLabels().getImage(jp)}; + } + } catch (Exception e) { + Log.log(e); + } + } + return NO_IMAGES; + } + + public StyledString getStyledText(BootDashModel element, BootDashColumn column) { + if (element != null) { + if (element.getRunTarget() != null) { + if (element.getRefreshState().isLoading()) { + StyledString prefix = new StyledString(); + if (element.getRefreshState().getMessage() != null) { + Color color = colorGrey(); + prefix = new StyledString(element.getRefreshState().getMessage() + " - ", stylers.italicColoured(color)); + } + return prefix.append(new StyledString(element.getRunTarget().getDisplayName(), stylers.italic())); + } else { + return new StyledString(element.getRunTarget().getDisplayName(), stylers.bold()); + } + } else { + return new StyledString(UNKNOWN_LABEL); + } + } + return stylers==null?new StyledString("null"):new StyledString("null", stylers.red()); + } + + public StyledString getStyledText(ButtonModel element) { + return new StyledString(element.getLabel(), stylers.hyperlink()); + } + + /** + * For a given column type return the styler to use for any [...] that are + * added around it. Return null + */ + public Styler getPrefixSuffixStyler(BootDashColumn column) { + if (column==TAGS) { + return stylers.tagBrackets(); + } else if (column==LIVE_PORT) { + Color portColor = colorGreen(); + return stylers.color(portColor); + } else if (column==INSTANCES) { + Color instancesColor = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getColorRegistry().get(ALT_TEXT_DECORATION_COLOR_THEME); + return stylers.color(instancesColor); + } else { + return Stylers.NULL; + } + } + + public StyledString getStyledText(BootDashElement element, BootDashColumn column) { + //The big case below should set either one of 'label' or'styledLabel', depending + // on whether it is 'styling capable'. + String label = null; + StyledString styledLabel = null; + + if (element != null) { + styledLabel = styledTextFromContributions(element, column); + if (styledLabel!=null) { + return styledLabel; + } + if (column==TAGS) { + String text = TagUtils.toString(element.getTags()); + styledLabel = stylers == null ? new StyledString(text) : TagUtils.applyTagStyles(text, stylers.tag()); + } else if (column==PROJECT) { + IJavaProject jp = element.getJavaProject(); + if (jp == null) { + // Not all projects in elements are Java projects. CF elements accept any project that contains a valid manifest.yml since the manifest.yml may + // point to an executable archive for the app (.jar/.war) + IProject project = element.getProject(); + if (project != null) { + label = project.getName(); + } else { + // Project and app (element) name are shown in separate columns now. If + // there is no project mapping + // do not show the element name anymore. That way the user knows that there is + // no mapping for that element. + label = ""; + } + } else { + styledLabel = getJavaLabels().getStyledText(jp); + //TODO: should use 'element.hasDevtools()' but its not implemented + // yet on CF elements. + boolean devtools = BootPropertyTester.hasDevtools(element.getProject()); + if (devtools) { + Color color = colorGreen(); + StyledString devtoolsDecoration = new StyledString(" [devtools]", stylers.color(color)); + styledLabel.append(devtoolsDecoration); + } + } + } else if (column==HOST) { + String host = element.getLiveHost(); + label = host == null ? UNKNOWN_LABEL : host; + } else if (column==TREE_VIEWER_MAIN) { + BootDashColumn[] cols = element.getColumns(); + styledLabel = new StyledString(); + for (BootDashColumn col : cols) { + //Ignore RUN_STATE_ICN because its already represented in the label's icon. + if (col != BootDashColumn.RUN_STATE_ICN) { + StyledString append = getStyledText(element, col); + if (hasText(append)) { + Styler styler = getPrefixSuffixStyler(col); + if (!hasText(styledLabel)) { + // Nothing in the label so far, don't added brackets to first piece + styledLabel = styledLabel.append(append); + } else { + if (col == BootDashColumn.DEFAULT_PATH || col == BootDashColumn.PROGRESS) { + styledLabel = styledLabel.append(" ").append(append); + } + else { + if (styler == null) { + styledLabel = styledLabel.append(" [").append(append).append("]"); + } else { + styledLabel = styledLabel.append(" [",styler).append(append).append("]",styler); + } + } + } + } + } + } + } else if (column==NAME) { + styledLabel = getStyleableName(element); + if (styledLabel == null) { + styledLabel = new StyledString(); + if (element.getName() != null) { + styledLabel.append(element.getName()); + } + else { + styledLabel.append(UNKNOWN_LABEL); + } + } + + if (element.getRefreshState() != null && element.getRefreshState().isLoading()) { + styledLabel = new StyledString(styledLabel.getString(), stylers.italicColoured(colorGrey())); + } + + } else if (column==PROGRESS) { + if (element.getRefreshState() != null && element.getRefreshState().isLoading()) { + String message = element.getRefreshState().getMessage(); + Color muted = colorGrey(); + if (StringUtils.hasText(message)) { + styledLabel = new StyledString("- " + message, stylers.italicColoured(muted)); + } else { + styledLabel = new StyledString("" + ELLIPSIS, stylers.italicColoured(muted)); + } + } else if (RunState.STARTING.equals(element.getRunState())) { + Color muted = colorGrey(); + styledLabel = new StyledString("- " + "Fetching runstate from JMX", stylers.italicColoured(muted)); + } + } else if (column==DEVTOOLS) { + if (element.hasDevtoolsDependency()) { + Color grey = colorGrey(); + Color green = colorGreen(); + Color color = element.isDevtoolsGreenColor() ? green : grey; + styledLabel = new StyledString("devtools", stylers.color(color)); + } else { + styledLabel = new StyledString(); + } + } else if (column==RUN_STATE_ICN) { + label = element.getRunState().toString(); + } else if (column==LIVE_PORT) { + RunState runState = element.getRunState(); + if (runState == RunState.RUNNING || runState == RunState.DEBUGGING) { + String textLabel; + ImmutableSet ports = element.getLivePorts(); + if (ports.isEmpty()) { + textLabel = "unknown port"; + } else { + StringBuilder str = new StringBuilder(); + String separator = ""; + for (Integer port : ports) { + str.append(separator); + str.append(":"); + str.append(port); + + separator = " "; + } + textLabel = str.toString(); + } + if (stylers == null) { + label = textLabel; + } else { + Color color = colorGreen(); + styledLabel = new StyledString(textLabel, stylers.color(color)); + } + } + } else if (column==DEFAULT_PATH) { + String path = element.getDefaultRequestMappingPath(); + if (stylers == null) { + label = path == null ? "" : path; + } else { + Color color = colorGrey(); + styledLabel = new StyledString(path == null ? "" : path, stylers.color(color)); + } + } else if (column==INSTANCES) { + int actual = element.getActualInstances(); + int desired = element.getDesiredInstances(); + boolean showDesired = desired >= 0; + if (!declutter || desired > 1 || actual > 1) { //Don't show: less clutter, you can already see whether a single instance is running or not + label = actual + ( showDesired ? "/" + desired : ""); + if (stylers != null) { + Color instancesColor = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getColorRegistry().get(ALT_TEXT_DECORATION_COLOR_THEME); + styledLabel = new StyledString(label, stylers.color(instancesColor)); + } + } + } else if (column==EXPOSED_URL) { + RunState runState = element.getRunState(); + if (runState == RunState.RUNNING || runState == RunState.DEBUGGING) { + List tunnelNames = new ArrayList<>(); + if (element instanceof AbstractLaunchConfigurationsDashElement) { + ImmutableSet launches = ((AbstractLaunchConfigurationsDashElement) element).getLaunchConfigs(); + for (ILaunchConfiguration launchConfig : launches) { + tunnelNames.add(launchConfig.getName()); + } + } + + for (String tunnelName : tunnelNames) { + NGROKClient ngrokClient = NGROKLaunchTracker.get(tunnelName); + if (ngrokClient != null) { + Color color = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getColorRegistry().get(ALT_TEXT_DECORATION_COLOR_THEME); + if (styledLabel == null) { + styledLabel = new StyledString("\u27A4 " + ngrokClient.getTunnel().getPublic_url(),stylers.color(color)); + } + else { + styledLabel.append(new StyledString(" / \u27A4 " + ngrokClient.getTunnel().getPublic_url(),stylers.color(color))); + } + } + } + + } + } else { + label = UNKNOWN_LABEL; + } + } + if (styledLabel!=null) { + return styledLabel; + } else if (label!=null) { + return new StyledString(label); + } + return new StyledString(""); + } + + private StyledString getStyleableName(BootDashElement element) { + if (element instanceof Styleable) { + return ((Styleable)element).getStyledName(stylers); + } + return null; + } + + private StyledString styledTextFromContributions(BootDashElement element, BootDashColumn col) { + initContributions(); + ImmutableCollection contributions = this.contributions.get(col); + for (Contribution c : contributions) { + StyledString styledText = c.getStyledText(stylers, element, col); + if (styledText!=null) { + return styledText; + } + } + return null; + } + + private ImmutableMultimap contributions = null; + + private void initContributions() { + if (contributions==null) { + ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); + for (Contribution c : injections.getBeans(Contribution.class)) { + for (BootDashColumn col : c.appliesTo) { + builder.put(col, c); + } + } + contributions = builder.build(); + } + } + + /** + * Deprecated: use getStyledText. + */ + @Deprecated + public String getText(BootDashElement element, BootDashColumn column) { + return getStyledText(element, column).getString(); + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + // private / helper stuff + + private boolean hasText(StyledString stext) { + return !stext.getString().isEmpty(); + } + + private AppearanceAwareLabelProvider getJavaLabels() { + if (javaLabels == null) { + javaLabels = new AppearanceAwareLabelProvider(); + } + return javaLabels; + } + + private Image[] getRunStateAnimation(RunState runState) { + try { + if (runStateImages==null) { + runStateImages = new RunStateImages(); + } + return runStateImages.getAnimation(runState); + } catch (Exception e) { + Log.log(e); + } + return null; + } + + /** + * Enables or disables decluttering. Decluttering means some + * 'obvious' information is hidden from generated label text. + *

+ * Decluttering is enabled by default. + */ + public BootDashLabels setDeclutter(boolean declutter) { + this.declutter = declutter; + return this; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/labels/RunStateImages.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/labels/RunStateImages.java new file mode 100644 index 000000000..b6c897d5d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/labels/RunStateImages.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.labels; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.model.RunState; + +/** + * @author Kris De Volder + */ +public class RunStateImages { + + private Map animations = new HashMap<>(); + + public synchronized Image[] getAnimation(RunState state) throws Exception { + Image[] anim = animations.get(state); + if (anim==null) { + String url = state.getImageUrl(); + animations.put(state, anim = createAnimation(url)); + } + return anim; + } + + private Image[] createAnimation(String urlString) throws Exception { + // For a png there might be animation frames to load (ImageLoader cannot + // pull out frames for an animated png) + // Given an input url of the form "foo.png" this will search + // for "foo_1.png", "foo_2.png" etc, until it can find no more frames + int dot = urlString.lastIndexOf('.'); + String prefix = urlString.substring(0, dot); + String suffix = urlString.substring(dot+1); + + List images = new ArrayList<>(); + int count = 1; + ImageDescriptor descriptor = null; + while ((descriptor = BootDashActivator.getImageDescriptor(prefix+"_"+Integer.toString(count++)+"."+suffix)) != null) { + images.add(descriptor.createImage()); + } + + if (images.size() != 0) { + // Animation frames were found, return them + return images.toArray(new Image[images.size()]); + } + else { + ImageDescriptor imageDescriptor = BootDashActivator.getImageDescriptor(urlString); + Image image; + if (imageDescriptor!=null) { + image = imageDescriptor.createImage(); + } else { + image = BootDashActivator.getImageDescriptor("/icons/rs_unknown.gif").createImage(); + } + return new Image[] {image}; + } + + +// input = cl.getResourceAsStream(urlString); +// try { +// ImageData[] data = loader.load(input); +// Image[] imgs = new Image[data.length]; +// for (int i = 0; i < imgs.length; i++) { +// imgs[i] = new Image(Display.getDefault(), data[i]); +// } +// return imgs; +// } finally { +// input.close(); +// } + } + + public void dispose() { + if (animations!=null) { + for (Image[] anim : animations.values()) { + if (anim!=null) { + for (Image image : anim) { + image.dispose(); + } + } + } + animations = null; + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/CommandInfo.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/CommandInfo.java new file mode 100644 index 000000000..68623dbd5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/CommandInfo.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2019 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.liveprocess; + +import java.util.Map; +import java.util.Map.Entry; + +public class CommandInfo { + + public final String command; + public final Map info; + + public CommandInfo(String command, Map info) { + super(); + this.command = command; + this.info = info; + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder("CommandInfo("+command+", {\n"); + for (Entry e : info.entrySet()) { + s.append(" "+e.getKey() +": "+e.getValue()+"\n"); + } + s.append("})"); + return s.toString(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java new file mode 100644 index 000000000..96a2ea319 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2019 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.liveprocess; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.lsp4e.LanguageServiceAccessor; +import org.eclipse.lsp4j.ExecuteCommandOptions; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.services.LanguageServer; + +import com.google.common.collect.ImmutableList; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@SuppressWarnings("restriction") +public final class DefaultLiveProcessCommandExecutor implements LiveProcessCommandsExecutor { + + private static final String CMD_LIST_PROCESSES = "sts/livedata/listProcesses"; + + private class DefaultServer implements Server { + private LanguageServer ls; + public DefaultServer(LanguageServer ls) { + this.ls = ls; + } + @Override + @SuppressWarnings("unchecked") + public Flux listCommands() { + return Mono.fromFuture(ls.getWorkspaceService().executeCommand(new ExecuteCommandParams( + CMD_LIST_PROCESSES, + ImmutableList.of() + ))) + .flatMapIterable(list -> (List)list) + .map(_cmdInfo -> { + Map map = (Map) _cmdInfo; + return new CommandInfo(map.get("action"), map); + }); + } + + @Override + public Mono executeCommand(CommandInfo cmd) { + return Mono.fromFuture(ls.getWorkspaceService().executeCommand(new ExecuteCommandParams( + cmd.command, + ImmutableList.of(cmd.info) + ))) + .then(); + } + } + + @Override + public List getLanguageServers() { + return LanguageServiceAccessor.getActiveLanguageServers(cap -> { + ExecuteCommandOptions commandCap = cap.getExecuteCommandProvider(); + if (commandCap!=null) { + List supportedCommands = commandCap.getCommands(); + return supportedCommands!=null && supportedCommands.contains(CMD_LIST_PROCESSES); + } + return false; + }) + .stream() + .map(DefaultServer::new) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveDataCapableElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveDataCapableElement.java new file mode 100644 index 000000000..45320bf81 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveDataCapableElement.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.liveprocess; + +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveDataConnectionManagementActions.ExecuteCommandAction; + +/** + * A BDE that has a correspondence to a process that can provide live data + * (either remote or local process) should implement this interface to allow + * the Boot Dash UI to filter elements and only show relevant live process + * actions when the corresponding element is selected. + *

+ * This interface also shows as a 'marker' to identify the types of elements + * for which the 'Live Data Connections...' context menu should be visible. + * + * @author Kris De Volder + */ +public interface LiveDataCapableElement { + boolean matchesLiveProcessCommand(ExecuteCommandAction action); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveDataConnectionManagementActions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveDataConnectionManagementActions.java new file mode 100644 index 000000000..622833d54 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveDataConnectionManagementActions.java @@ -0,0 +1,207 @@ +/******************************************************************************* + * Copyright (c) 2019 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.liveprocess; + +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.resource.ImageDescriptor; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveProcessCommandsExecutor.Server; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction.Params; +import org.springframework.ide.eclipse.boot.dash.views.sections.DynamicSubMenuSupplier; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import reactor.core.publisher.Flux; + +public class LiveDataConnectionManagementActions extends AbstractDisposable implements DynamicSubMenuSupplier { + + private static final IAction DUMMY_ACTION = new Action("No matching processes") { + { + setEnabled(false); + } + }; + private final Params params; + private LiveProcessCommandsExecutor liveProcessCmds; + private final LiveExpression isEnabled; + + @Override + public String getLabel() { + return "Live Data Connections..."; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return BootDashActivator.getImageDescriptor("icons/light-bulb.png"); + } + + @Override + public ImageDescriptor getDisabledImageDescriptor() { + return BootDashActivator.getImageDescriptor("icons/light-bulb-disabled.png"); + } + + @Override + public boolean isVisible() { + if (liveProcessCmds!=null) { + Set selection = params.getSelection().getValue(); + return selection.isEmpty() || selection.stream().anyMatch(x -> x instanceof LiveDataCapableElement); + } + return false; + } + + public class ExecuteCommandAction extends AbstractBootDashElementsAction { + private String projectName; + private String label; + private Server server; + private CommandInfo commandInfo; + + public ExecuteCommandAction(Server server, CommandInfo commandInfo) { + super(params); + this.server = server; + this.commandInfo = commandInfo; + String command = commandInfo.command; + int lastSlash = command.lastIndexOf("/"); + String humanReadableCommand = command.substring(lastSlash+1); + humanReadableCommand = humanReadableCommand.substring(0, 1).toUpperCase() + humanReadableCommand.substring(1); + this.projectName = commandInfo.info.get("projectName"); + label = humanReadableCommand + " "+commandInfo.info.get("label"); + this.setText(label); + } + + public String getProjectName() { + return projectName; + } + + @Override + public String toString() { + return "ExecuteCommandAction [projectName=" + projectName +", label="+label+"]"; + } + + @Override + public void updateVisibility() { + this.setVisible(true); + } + + @Override + public void updateEnablement() { + this.setEnabled(true); + } + + @Override + public void run() { + try { + server.executeCommand(commandInfo).block(Duration.ofSeconds(2)); + } catch (Exception e) { + Log.log(e); + } + } + + public String getProcessId() { + return commandInfo.info.get("processId"); + } + } + + public LiveDataConnectionManagementActions(Params params) { + this.params = params; + this.liveProcessCmds = params.getLiveProcessCmds(); + ObservableSet selection = params.getSelection().getElements(); + this.isEnabled = addDisposableChild(new LiveExpression(false) { + + ElementStateListener elementStateListener = (BootDashElement e) -> { + refresh(); + }; + + { + dependsOn(selection); + params.getModel().addElementStateListener(elementStateListener); + } + + @Override + protected Boolean compute() { + ImmutableSet els = selection.getValues(); + if (els.isEmpty()) { + return true; + } else { + for (BootDashElement bde : els) { + if (bde instanceof LiveDataCapableElement) { + RunState s = bde.getRunState(); + if (s == RunState.RUNNING || s ==RunState.DEBUGGING) { + return true; + } + } + } + return false; + } + } + }); + } + + @Override + public List getActions() { + Set bdes = params.getSelection().getValue(); + Predicate filter; + if (bdes.isEmpty()) { + filter = x -> true; + } else { + filter = action -> { + for (BootDashElement bde : bdes) { + if (bde instanceof LiveDataCapableElement) { + if (((LiveDataCapableElement)bde).matchesLiveProcessCommand(action)) { + return true; + } + } + } + return false; + }; + } + try { + List servers = liveProcessCmds.getLanguageServers(); + return Flux.fromIterable(servers) + .flatMap((Server server) -> + server.listCommands() + .map(cmdInfo -> new ExecuteCommandAction(server, cmdInfo)) + ) + .filter(filter) + .cast(IAction.class) + .collect(Collectors.toList()) + .map(actions -> { + if (actions.isEmpty()) { + return ImmutableList.of(DUMMY_ACTION); + } + return actions; + }) + .block(); + } catch (Exception e) { + Log.log(e); + } + return ImmutableList.of(DUMMY_ACTION); + } + + @Override + public LiveExpression isEnabled() { + return isEnabled; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveProcessCommandsExecutor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveProcessCommandsExecutor.java new file mode 100644 index 000000000..1f51d001f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveProcessCommandsExecutor.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2019 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.liveprocess; + +import java.util.List; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface LiveProcessCommandsExecutor { + + interface Server { + Flux listCommands(); + Mono executeCommand(CommandInfo cmd); + } + + static LiveProcessCommandsExecutor getDefault() { + return new DefaultLiveProcessCommandExecutor(); + } + + List getLanguageServers(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/DelegatingLiveSet.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/DelegatingLiveSet.java new file mode 100644 index 000000000..4c85aa1ef --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/DelegatingLiveSet.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2015 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.livexp; + +import java.util.Collections; +import java.util.Set; + +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +import com.google.common.collect.ImmutableSet; + +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +/** + * A {@link ObservableSet} which is defined as mirroring the + * contents of a delegate. + *

+ * This delegate is initially null and can be set repeatedly. + *

+ * When delegate is null the DelegatingSet will be empty, when set + * to point to non-null ObservableSet then its contents will be equal to the + * contents of the delegate. + * + * @author Kris De Volder + */ +public class DelegatingLiveSet extends ObservableSet { + + public DelegatingLiveSet() { + } + + private ObservableSet delegate = null; + private ValueListener> delegateListener = new ValueListener>() { + public void gotValue(LiveExpression> exp, ImmutableSet value) { + refresh(); + } + }; + + @Override + protected ImmutableSet compute() { + if (delegate==null) { + return ImmutableSet.of(); + } else { + return delegate.getValue(); + } + } + + public synchronized void setDelegate(ObservableSet newDelegate) { + LiveExpression> oldDelegate = this.delegate; + this.delegate = newDelegate; + if (oldDelegate==newDelegate) { + return; + } else { + if (oldDelegate!=null) { + oldDelegate.removeListener(delegateListener); + } + if (newDelegate==null) { + //trigger a refresh because the delegate changed and the newDelegate won't trigger one + refresh(); + } else { + newDelegate.addListener(delegateListener); + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/DisposingFactory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/DisposingFactory.java new file mode 100644 index 000000000..95bd49a45 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/DisposingFactory.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright (c) 2015 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.livexp; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +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.ValueListener; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +import com.google.common.collect.ImmutableSet; + +/** + * A disposing factory creates objects of some type V based on some + * parameter of type K. It guarantees that the same element is returned + * when the same key is passed in as argument, provided that the + * key is one of the currently 'validKeys'. + *

+ * The factory is also responsible for monitoring the set of 'validKeys' and calling + * the dispose method on the values associated with keys that are no longer + * valid. + * + * @author Kris De Volder + */ +public abstract class DisposingFactory implements Disposable { + + private ObservableSet validKeys; + private Map cachedInstances = new HashMap<>(); + private ValueListener> validKeyListener = null; + + public DisposingFactory(ObservableSet validKeys) { + this.validKeys = validKeys; + validKeys.onDispose(new DisposeListener() { + public void disposed(Disposable disposed) { + DisposingFactory.this.dispose(); + } + }); + } + + protected abstract V create(K key); + + public synchronized V createOrGet(K key) { + ImmutableSet valid = getValidKeys(); + if (valid.contains(key)) { + enableValidKeyTracking(); + V instance = cachedInstances.get(key); + if (instance==null) { + instance=create(key); + if (instance!=null) { + cachedInstances.put(key, instance); + } + } + return instance; + } + return null; + } + + private void enableValidKeyTracking() { + if (validKeyListener==null) { + validKeys.addListener(validKeyListener = new ValueListener>() { + public void gotValue(LiveExpression> exp, ImmutableSet value) { + retainOnlyValidKeys(); + } + }); + } + + } + + private ImmutableSet getValidKeys() { + if (validKeys!=null) { + return validKeys.getValues(); + } + return ImmutableSet.of(); + } + + @Override + public synchronized void dispose() { + if (validKeys!=null) { + if (validKeyListener!=null) { + validKeys.removeListener(validKeyListener); + } + validKeys = null; + retainOnlyValidKeys(); + cachedInstances = null; + } + } + + private synchronized void retainOnlyValidKeys() { + if (cachedInstances!=null) { + ImmutableSet valid = getValidKeys(); + Iterator> iter = cachedInstances.entrySet().iterator(); + while (iter.hasNext()) { + Entry e = iter.next(); + K k = e.getKey(); + if (valid.contains(k)) { + //keep + } else { + e.getValue().dispose(); + iter.remove(); + } + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/ElementwiseListener.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/ElementwiseListener.java new file mode 100644 index 000000000..20879344e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/ElementwiseListener.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2015 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.livexp; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +import com.google.common.collect.ImmutableSet; + +/** + * An 'adapter' class that can used to create a ValueListener on a LiveSet which provides methods + * that get called when individual elements get added or removed to the set (whereas the + * general value listener treats the set itself as a value), + * + * @author Kris De Volder + */ +public abstract class ElementwiseListener implements ValueListener> { + + private Set lastState = Collections.emptySet(); + + @Override + public final void gotValue(LiveExpression> exp, ImmutableSet newState) { + Set removed; + Set added; + synchronized (this) { + //compute the 'diffs' inside synch block + //newState = new LinkedHashSet(newState); //use a 'snapshot' because the set may change. + added = minus(newState, lastState); + removed = minus(lastState, newState); + lastState = newState; + } + + //TODO: race conditions sending events if set is changed in multiple threads? + + //sending 'diff' events outside sync block. + for (T t : removed) { + removed(exp, t); + } + for (T t : added) { + added(exp, t); + } + } + + protected abstract void added(LiveExpression> exp, T e); + protected abstract void removed(LiveExpression> exp, T e); + + private Set minus(Set set, Set subtract) { + LinkedHashSet diff = new LinkedHashSet<>(set); + diff.removeAll(subtract); + return diff; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/MappedValuesSet.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/MappedValuesSet.java new file mode 100644 index 000000000..4fb13f6f8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/MappedValuesSet.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2015 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.livexp; + +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; +import com.google.common.collect.Sets; +import com.google.common.collect.Sets.SetView; + +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +/** + * An observable set that contains the results of applying a mapping function onto + * the values of an ObservableSet of LiveExps. + */ +public abstract class MappedValuesSet extends ObservableSet { + + private final ObservableSet> target; + private ImmutableSet> listenersAttached = ImmutableSet.of(); + + private ValueListener valueListener = new ValueListener() { + @Override + public void gotValue(LiveExpression exp, T value) { + MappedValuesSet.this.refresh(); + } + }; + + private ValueListener>> setListener = new ValueListener>>() { + public void gotValue(LiveExpression>> exp, ImmutableSet> value) { + refreshValueListeners(); + } + }; + + public MappedValuesSet(ObservableSet> target) { + this.target = target; + this.target.addListener(setListener); + } + + /** + * Override to define the function to apply to each value before adding to the result set. + */ + protected abstract R applyFun(T arg); + + /** + * Called when the target set changes. This should add and remove valuelistener to + * the LiveExps in the target to ensure we listen each expression. + */ + private synchronized void refreshValueListeners() { + ImmutableSet> current = target.getValues(); + SetView> removed = Sets.difference(listenersAttached, current); + SetView> added = Sets.difference(current, listenersAttached); + for (LiveExpression exp : removed) { + exp.removeListener(valueListener); + } + for (LiveExpression exp : added) { + exp.addListener(valueListener); + } + listenersAttached = current; + } + + @Override + protected ImmutableSet compute() { + ImmutableSet.Builder builder = immutableSetBuilder(); + for (LiveExpression exp : target.getValues()) { + R val = applyFun(exp.getValue()); + if (val!=null) { + builder.add(val); + } + } + return builder.build(); + } + + /** + * By overriding this method, subclass can determine the kind of ImmutableSet that will + * be constructed to hold the mapped values. For example, a subclass may want to use ImmutableSortedSet instead + * of a plain ImmutableSet which iterates elements in unpredictable order. + */ + protected ImmutableSet.Builder immutableSetBuilder() { + //TODO: pull up to superclass and use consistently anywhere a ObservableSet needs to + // construct a immutable set? + return ImmutableSet.builder(); + } + + @Override + public void dispose() { + synchronized (this) { + if (listenersAttached!=null) { + for (LiveExpression exp : listenersAttached) { + exp.removeListener(valueListener); + } + listenersAttached = null; + } + if (setListener!=null) { + target.removeListener(setListener); + setListener = null; + } + } + super.dispose(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/MultiSelection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/MultiSelection.java new file mode 100644 index 000000000..8db4d1a50 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/MultiSelection.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) 2013 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.livexp; + +import java.util.Set; + +import org.eclipse.core.runtime.Assert; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSets; + +/** + * Represents a selection of zero or more elements of type T in some UI component. + * + * @author Kris De Volder + */ +public final class MultiSelection { + + public static MultiSelection empty(Class type) { + return new MultiSelection<>(type, LiveSets.emptySet(type)); + } + + /** + * Converts a 'single' selection to a multi selection. + */ + public static MultiSelection singletonOrEmpty(Class type, + LiveExpression singleSelection) { + return new MultiSelection<>(type, org.springsource.ide.eclipse.commons.livexp.core.LiveSets.singletonOrEmpty(singleSelection)); + } + + + + public static MultiSelection union(MultiSelection a, MultiSelection b) { + Assert.isLegal(a.getElementType().equals(b.getElementType())); + return from(a.getElementType(), LiveSets.union(a.getElements(), b.getElements())); + } + + private final Class elementType; + private final ObservableSet elements; + + public MultiSelection(Class elementType, ObservableSet elements) { + this.elementType = elementType; + this.elements = elements; + } + + public Class getElementType() { + return elementType; + } + + /** + * Filter a selection to retain only elements of a given type. + */ + public MultiSelection filter(Class retainType) { + MultiSelection converted = this.as(retainType); + if (converted!=null) { + //Don't need to filter element-by-element since the selection only + // can contain elements of 'retainType'. + return converted; + } else { + //Selection may contain objects that are not instances of retainType. + return from(retainType, org.springsource.ide.eclipse.commons.livexp.core.LiveSets.filter(getElements(), retainType)); + } + } + + /** + * Convert a selection of one type into a selection of a different + * type. The conversion only succeeds if the target-type is assignment + * compatible with the source type. + * + * @return Converted selection or null if the conversion is not legal. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public MultiSelection as(Class toElementType) { + if (toElementType.isAssignableFrom(elementType)) { + return new MultiSelection(toElementType, ((ObservableSet) elements)); + } else { + return null; + } + } + + /** + * Convert a selection of one type into a selection of a different + * type. The conversion only succeeds if the target-type is assignment + * compatible with the source type. + * + * @return Converted selection + */ + public MultiSelection cast(Class toElementType) throws ClassCastException { + MultiSelection converted = as(toElementType); + if (converted==null) { + throw new ClassCastException(getElementType().getName()+" => "+toElementType.getName()); + } + return converted; + } + + + public ObservableSet getElements() { + return elements; + } + + public static MultiSelection from(Class type, ObservableSet elements) { + return new MultiSelection<>(type, elements); + } + + public Set getValue() { + return getElements().getValue(); + } + + /** + * @return The only element in the selection, if exactly one element is selected; or null + * otherwise. + */ + public T getSingle() { + Set es = getValue(); + if (es.size()==1) { + for (T t : es) { + return t; + } + } + return null; + } + + /** + * Converts a 'multi' selection to a selection of a single element. The single selection + * will be the selected element of the multi-selection if exactly one element is + * currently selected, and 'null' otherwise. + */ + public LiveExpression toSingleSelection() { + LiveExpression singleSelect = new LiveExpression() { + protected T compute() { + return MultiSelection.this.getSingle(); + } + }; + singleSelect.dependsOn(getElements()); + return singleSelect; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/MultiSelectionSource.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/MultiSelectionSource.java new file mode 100644 index 000000000..e1c38e88f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/MultiSelectionSource.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2015 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.livexp; + +import org.springsource.ide.eclipse.commons.livexp.ui.IPageSection; + +/** + * Page sections can be a source of selections (meaning user can select elements in them). + * If so, they should implement this interface. This will allow pages or other sections to obtain + * their selection and listen to changes on the selection. + * + * @author Kris De Volder + */ +public interface MultiSelectionSource extends IPageSection { + MultiSelection getSelection(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/PollingLiveExp.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/PollingLiveExp.java new file mode 100644 index 000000000..71a5ec4f6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/PollingLiveExp.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.livexp; + +import java.time.Duration; +import java.util.function.Supplier; + +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.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +/** + * LiveExpression that continually refreshes itself at regular intervals with a background job. + *

+ * The expression, when created, starts out in a 'sleeping' state. It will not start refreshing/computing + * its value until methods like 'refreshOnce', 'refreshFor' or 'refreshForever' are called. + * + * @author Kris De Volder + */ +public abstract class PollingLiveExp extends LiveExpression { + + private static final Supplier STOP_REFRESHING = () -> false; + + private Job refreshJob = createRefreshJob(); + + /** + * Time in ms to 'sleep' between refreshes. + */ + private long sleepBetweenRefreshes = 500; + + private Supplier continueRefreshing = STOP_REFRESHING; + + public PollingLiveExp(T initialValue) { + super(initialValue); + } + + /** + * Override the default 'sleepBetweenRefreshes' value. + */ + public PollingLiveExp sleepBetweenRefreshes(Duration duration) { + this.sleepBetweenRefreshes = duration.toMillis(); + return this; + } + + private Job createRefreshJob() { + Job job = new Job("Refresh PollingLiveExp") { + @Override + protected IStatus run(IProgressMonitor monitor) { + refresh(); + if (continueRefreshing.get()) { + this.schedule(sleepBetweenRefreshes); + } + return Status.OK_STATUS; + } + }; + job.setSystem(true); + return job; + } + + @Override + public void refresh() { + if (continueRefreshing.get()) { + super.refresh(); + } + } + + @Override + public void dispose() { + refreshJob = null; + continueRefreshing = STOP_REFRESHING; + super.dispose(); + } + + /** + * Start refreshing now, and continue until given duration expires. + */ + public void refreshFor(Duration duration) { + Job job = refreshJob; + if (job!=null) { + long stopRefrestingAfter = System.currentTimeMillis() + duration.toMillis(); + this.continueRefreshing = () -> System.currentTimeMillis() <= stopRefrestingAfter; + job.schedule(); + } + } + + /** + * Start refreshing now, and continue forever (or until this expression is disposed). + */ + public PollingLiveExp refreshForever() { + Job job = refreshJob; + if (job!=null) { + continueRefreshing = () -> true; + job.schedule(); + } + return this; + } + + public PollingLiveExp refreshOnce() { + Job job = refreshJob; + if (job!=null) { + continueRefreshing = new Supplier() { + + boolean firstTime = true; + + @Override + public Boolean get() { + try { + return firstTime; + } finally { + firstTime = false; + } + } + }; + job.schedule(); + } + return this; + } + + /** + * Lambda-friendly way of creating a PollingLiveExp instance. + */ + public static PollingLiveExp create(T initialValue, Supplier computer) { + return new PollingLiveExp(initialValue) { + @Override + protected T compute() { + return computer.get(); + } + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/README.txt b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/README.txt new file mode 100644 index 000000000..52f72a585 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/livexp/README.txt @@ -0,0 +1,3 @@ +This package contains some extensions and utilities to livexp. They may or may not be useful in +a broader context. So it might make sense to copy and/or generalize them to add to livexp +in the future. \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/metadata/PropertyStoreFactory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/metadata/PropertyStoreFactory.java new file mode 100644 index 000000000..881ff9b52 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/metadata/PropertyStoreFactory.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.metadata; + +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.model.DefaultSecuredCredentialsStore; +import org.springframework.ide.eclipse.boot.dash.model.SecuredCredentialsStore; +import org.springsource.ide.eclipse.commons.core.pstore.IScopedPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PreferenceBasedStore; + +public class PropertyStoreFactory { + + public static SecuredCredentialsStore createSecuredCredentialsStore() { + return new DefaultSecuredCredentialsStore(); + } + + public static IScopedPropertyStore createForRunTargets() { + return new PreferenceBasedStore() { + protected IEclipsePreferences createPrefs(RunTargetType runTargetType) { + return InstanceScope.INSTANCE.getNode(BootDashActivator.PLUGIN_ID + ':' + runTargetType.getName()); + } + }; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractBootDashModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractBootDashModel.java new file mode 100644 index 000000000..6329c91a6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractBootDashModel.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.model; + +import java.util.Comparator; + +import org.eclipse.core.runtime.ListenerList; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.views.BootDashModelConsoleManager; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSets; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +public abstract class AbstractBootDashModel extends AbstractDisposable implements BootDashModel { + + private static final boolean DEBUG = false; +// (""+Platform.getLocation()).contains("bamboo") || +// (""+Platform.getLocation()).contains("kdvolder") +// ; + private final BootDashViewModel parent; + private final RunTarget target; + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + public AbstractBootDashModel(RunTarget target, BootDashViewModel parent) { + super(); + this.target = target; + this.parent = parent; + } + + public RunTarget getRunTarget() { + return this.target; + } + + ListenerList elementStateListeners = new ListenerList(); + + @Override + public void notifyElementChanged(BootDashElement element, Object reason) { + debug("notifyElementChanged("+element.getName() + ", "+reason); + if (element!=null) { + for (Object l : elementStateListeners.getListeners()) { + ((BootDashModel.ElementStateListener) l).stateChanged(element); + } + } + } + + protected LiveExpression createRefreshState() { + return LiveExpression.constant(RefreshState.READY); + } + + ListenerList modelStateListeners = new ListenerList(); + + @Override + public final void notifyModelStateChanged() { + for (Object l : modelStateListeners.getListeners()) { + ((BootDashModel.ModelStateListener) l).stateChanged(this); + } + } + + @Override + public Comparator getElementComparator() { + return null; + } + + abstract public ObservableSet getElements(); + + abstract public BootDashModelConsoleManager getElementConsoleManager(); + + /** + * Trigger manual model refresh. + */ + @Override + abstract public void refresh(UserInteractions ui); + + public void addElementStateListener(BootDashModel.ElementStateListener l) { + elementStateListeners.add(l); + } + + public void removeElementStateListener(BootDashModel.ElementStateListener l) { + elementStateListeners.remove(l); + } + + public void addModelStateListener(BootDashModel.ModelStateListener l) { + modelStateListeners.add(l); + } + + public void removeModelStateListener(BootDashModel.ModelStateListener l) { + modelStateListeners.remove(l); + } + + public BootDashViewModel getViewModel() { + return parent; + } + + @Override + public String getDisplayName() { + return getRunTarget().getDisplayName(); + } + + @Override + public String getNameTemplate() { + return getRunTarget().getNameTemplate(); + } + + @Override + public void setNameTemplate(String template) throws Exception { + getRunTarget().setNameTemplate(template); + } + + @Override + public boolean hasCustomNameTemplate() { + return getRunTarget().hasCustomNameTemplate(); + } + + public ObservableSet getButtons() { + return LiveSets.emptySet(ButtonModel.class); + } + + @Override + public final void performDoubleClickAction(UserInteractions ui) { + getRunTarget().performDoubleClickAction(ui); + } + + protected final SimpleDIContext injections() { + return getViewModel().getContext().injections; + } + + public final UserInteractions ui() { + return injections().getBean(UserInteractions.class); + } + + protected final PropertyStoreApi getPersistentProperties() { + return target.getPersistentProperties(); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractLaunchConfigurationsDashElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractLaunchConfigurationsDashElement.java new file mode 100644 index 000000000..24e39bea7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractLaunchConfigurationsDashElement.java @@ -0,0 +1,813 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.model; + +import java.time.Duration; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.ui.IDebugUIConstants; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.launching.SocketUtil; +import org.eclipse.swt.widgets.Display; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansModel; +import org.springframework.ide.eclipse.boot.dash.livexp.PollingLiveExp; +import org.springframework.ide.eclipse.boot.dash.model.actuator.ActuatorClient; +import org.springframework.ide.eclipse.boot.dash.model.actuator.JMXActuatorClient; +import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.LiveEnvModel; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKClient; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKLaunchTracker; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKTunnel; +import org.springframework.ide.eclipse.boot.dash.util.CollectionUtils; +import org.springframework.ide.eclipse.boot.dash.util.LaunchConfRunStateTracker; +import org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.RunStateListener; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.cli.CloudCliServiceLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.util.BootDebugUITools; +import org.springframework.ide.eclipse.boot.launch.util.BootLaunchUtils; +import org.springframework.ide.eclipse.boot.launch.util.SpringApplicationLifeCycleClientManager; +import org.springframework.ide.eclipse.boot.launch.util.SpringApplicationLifecycleClient; +import org.springframework.ide.eclipse.boot.util.RetryUtil; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; +import org.springsource.ide.eclipse.commons.frameworks.core.maintype.MainTypeFinder; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression; +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.ui.Disposable; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; +import org.springsource.ide.eclipse.commons.ui.launch.LaunchUtils; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * Abstracts out the commonalities between {@link BootProjectDashElement} and {@link LaunchConfDashElement}. Each can + * be viewed as representing a collection of launch configuration. + *

+ * A {@link BootProjectDashElement} element represents all the launch configurations associated with a given project whereas as + * {@link LaunchConfDashElement} represent a single launch configuration (i.e. a singleton collection). + * + * @author Kris De Volder + */ +public abstract class AbstractLaunchConfigurationsDashElement extends WrappingBootDashElement implements Duplicatable { + + private static final boolean DEBUG = false; //DebugUtil.isDevelopment(); + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + public static final EnumSet READY_STATES = EnumSet.of(RunState.RUNNING, RunState.DEBUGGING); + + private static final Duration LIVE_DATA_REFRESH_TIMEOUT = Duration.ofMinutes(2); + + private LiveExpression runState; + private LiveExpression livePort; + private LiveExpression actuatorPort; + private LiveExpression actualInstances; + + private PropertyStoreApi persistentProperties; + + private PollingLiveExp>> liveRequestMappings; + private PollingLiveExp> liveBeans; + private PollingLiveExp> liveEnv; + + public AbstractLaunchConfigurationsDashElement(LocalBootDashModel bootDashModel, T delegate) { + super(bootDashModel, delegate); + this.runState = createRunStateExp(); + this.livePort = createPortExpression(runState); + this.actuatorPort = createActuatorPortExpression(runState); + this.actualInstances = createActualInstancesExp(); + addElementNotifier(livePort); + addElementNotifier(runState); + addElementNotifier(actualInstances); + } + + protected abstract IPropertyStore createPropertyStore(); + + @Override + public abstract ImmutableSet getLaunchConfigs(); + + @Override + public abstract IProject getProject(); + + @Override + public abstract String getName(); + + @Override + public RunState getRunState() { + return runState.getValue(); + } + + public LiveExpression getRunStateExp() { + return runState; + } + + @Override + public String toString() { + return this.getClass().getSimpleName()+"("+getName()+")"; + } + + @Override + public int getLivePort() { + return livePort.getValue(); + } + + @Override + public String getLiveHost() { + return "localhost"; + } + + @Override + public ILaunchConfiguration getActiveConfig() { + ILaunchConfiguration single = CollectionUtils.getSingle(getLaunchConfigs()); + if (single!=null) { + return single; + } + return null; + } + + @Override + public void stop() throws Exception { + stop(true); + } + + public void stop(boolean sync) throws Exception { + debug("Stopping: "+this+" "+(sync?"...":"")); + final CompletableFuture done = sync?new CompletableFuture<>():null; + try { + ImmutableSet launches = getLaunches(); + if (sync) { + LaunchUtils.whenTerminated(launches, new Runnable() { + public void run() { + done.complete(null); + } + }); + } + try { + BootLaunchUtils.terminate(launches); + shutdownExpose(); + } catch (Exception e) { + //why does terminating process with Eclipse debug UI fail so #$%# often? + Log.log(new Error("Termination of "+this+" failed", e)); + } + } catch (Exception e) { + Log.log(e); + } + if (sync) { + //Eclipse waits for 5 seconds before timing out. So we use a similar timeout but slightly + // larger. Windows case termination seem to fail silently sometimes so its up to us + // to handle here. + done.get(6, TimeUnit.SECONDS); + debug("Stopping: "+this+" "+"DONE"); + } + } + + /** + * Get the launches associated with this element. + *

+ * Note, we could implement it here by taking the union of all launches for all launch confs, + * but subclass can provide more efficient implementation so we make this abstract. + */ + public abstract ImmutableSet getLaunches(); + + @Override + public void restart(RunState runningOrDebugging, UserInteractions ui) throws Exception { + switch (runningOrDebugging) { + case RUNNING: + restart(ILaunchManager.RUN_MODE, ui); + break; + case DEBUGGING: + restart(ILaunchManager.DEBUG_MODE, ui); + break; + default: + throw new IllegalArgumentException("Restart expects RUNNING or DEBUGGING as 'goal' state"); + } + } + + public void restart(final String runMode, UserInteractions ui) throws Exception { + stopSync(); + start(runMode, ui); + } + + public void stopSync() throws Exception { + try { + stop(true); + } catch (TimeoutException e) { + Log.info("Termination of '"+this.getName()+"' timed-out. Retrying"); + //Try it one more time. On windows this times out occasionally... and then + // it works the next time. + stop(true); + } + } + + private void start(final String runMode, UserInteractions ui) { + try { + ILaunchConfiguration conf = getOrCreateLaunchConfig(ui); + if (conf!=null) { + launch(runMode, conf); + } + } catch (Exception e) { + Log.log(e); + } + } + + private ILaunchConfiguration getOrCreateLaunchConfig(UserInteractions ui) throws Exception { + ILaunchConfiguration conf = null; + + ImmutableSet configs = getLaunchConfigs(); + if (configs.isEmpty()) { + IType mainType = chooseMainType(ui); + if (mainType!=null) { + LocalRunTarget target = getTarget(); + IJavaProject jp = getJavaProject(); + conf = target.createLaunchConfig(jp, mainType); + } + } else { + conf = chooseConfig(ui, configs); + } + + return conf; + } + + private IType chooseMainType(UserInteractions ui) throws CoreException { + IType[] mainTypes = guessMainTypes(); + if (mainTypes.length==0) { + ui.errorPopup("Problem launching", "Couldn't find a main type in '"+getName()+"'"); + return null; + } else if (mainTypes.length==1){ + return mainTypes[0]; + } else { + return ui.chooseMainType(mainTypes, "Choose Main Type", "Choose main type for '"+getName()+"'"); + } + } + + protected IType[] guessMainTypes() throws CoreException { + return MainTypeFinder.guessMainTypes(getJavaProject(), new NullProgressMonitor()); + } + + protected void launch(final String runMode, final ILaunchConfiguration conf) throws Exception { + CompletableFuture done = new CompletableFuture<>(); + Display.getDefault().syncExec(new Runnable() { + public void run() { + try { + BootDebugUITools.launchInBackground(conf, runMode, done); + } catch (Throwable e) { + done.completeExceptionally(e); + } + } + }); + Display display = Display.getCurrent(); + if (display!=null) { + //Blocking the ui thread is iffy. It has a tendency to deadlock when + // work you are waiting for is actually using 'syncExec or asyncExec' somewhere inside. + //We can avoid this deadlock by calling on display.readAndDispatch to allow other stuff to run in + //the ui thread while we are waiting. + while (!done.isDone() ) { + while (display.readAndDispatch()) {} + done.get(100, TimeUnit.MILLISECONDS); + } + } + done.get(); + } + + @Override + public void openConfig(UserInteractions ui) { + try { + IProject p = getProject(); + if (p!=null) { + ILaunchConfiguration conf; + ImmutableSet configs = getLaunchConfigs(); + if (configs.isEmpty()) { + conf = createLaunchConfigForEditing(); + } else { + conf = chooseConfig(ui, configs); + } + if (conf!=null) { + ui.openLaunchConfigurationDialogOnGroup(conf, getLaunchGroup()); + } + } + } catch (Exception e) { + ui.errorPopup("Couldn't open config for "+getName(), ExceptionUtil.getMessage(e)); + } + } + + @Override + public boolean canDuplicate() { + return getLaunchConfigs().size()==1; + } + + @Override + public LaunchConfDashElement duplicate(UserInteractions ui) { + try { + ILaunchConfiguration conf = CollectionUtils.getSingle(getLaunchConfigs()); + if (conf!=null) { + ILaunchConfiguration newConf = BootLaunchConfigurationDelegate.duplicate(conf); + return getBootDashModel().getLaunchConfElementFactory().createOrGet(newConf); + } + } catch (Exception e) { + Log.log(e); + ui.errorPopup("Couldn't duplicate config", ExceptionUtil.getMessage(e)); + } + return null; + } + + @Override + public int getDesiredInstances() { + //special case for no launch configs (a single launch conf is created on demand, + //so we should treat it as if it already has one). + return Math.max(1, getLaunchConfigs().size()); + } + + @Override + public int getActualInstances() { + return actualInstances.getValue(); + } + + @Override + public PropertyStoreApi getPersistentProperties() { + if (persistentProperties==null) { + IPropertyStore backingStore = createPropertyStore(); + this.persistentProperties = PropertyStores.createApi(backingStore); + } + return persistentProperties; + } + + private LaunchConfRunStateTracker runStateTracker() { + return getBootDashModel().getLaunchConfRunStateTracker(); + } + + protected void refreshRunState() { + runState.refresh(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public ILaunchConfiguration createLaunchConfigForEditing() throws Exception { + IJavaProject jp = getJavaProject(); + LocalRunTarget target = getTarget(); + IType[] mainTypes = guessMainTypes(); + return target.createLaunchConfig(jp, mainTypes.length==1?mainTypes[0]:null); + } + + protected ILaunchConfiguration chooseConfig(UserInteractions ui, Collection configs) { + //TODO: this should probably be removed. Actions etc. should either apply to all the elements at once, + // or be disabled if that seems ill-conceived. In such a ui there should be no need to popup a dialog + // to choose a configuration. + ILaunchConfiguration conf = chooseConfigurationDialog(configs, + "Choose Launch Configuration", + "Several launch configurations are associated with '"+getName()+"' "+ + "Choose one.", ui); + return conf; + } + + private ILaunchConfiguration chooseConfigurationDialog(Collection configs, String dialogTitle, String message, UserInteractions ui) { + if (configs.size()==1) { + return CollectionUtils.getSingle(configs); + } else if (configs.size()>0) { + ILaunchConfiguration chosen = ui.chooseConfigurationDialog(dialogTitle, message, configs); + return chosen; + } + return null; + } + + private String getLaunchGroup() { + switch (getRunState()) { + case RUNNING: + return IDebugUIConstants.ID_RUN_LAUNCH_GROUP; + case DEBUGGING: + return IDebugUIConstants.ID_DEBUG_LAUNCH_GROUP; + default: + return IDebugUIConstants.ID_DEBUG_LAUNCH_GROUP; + } + } + + public int getActuatorPort() { + return actuatorPort.getValue(); + } + + protected LiveExpression createRunStateExp() { + final LaunchConfRunStateTracker tracker = runStateTracker(); + final LiveExpression exp = new LiveExpression() { + protected RunState compute() { + AbstractLaunchConfigurationsDashElement it = AbstractLaunchConfigurationsDashElement.this; + debug("Computing runstate for "+it); + LaunchConfRunStateTracker tracker = runStateTracker(); + RunState state = RunState.INACTIVE; + for (ILaunchConfiguration conf : getLaunchConfigs()) { + RunState confState = tracker.getState(conf); + debug("state for conf "+conf+" = "+confState); + state = state.merge(confState); + } + debug("runstate for "+it+" => "+state); + return state; + } + @Override + public String toString() { + return "LiveExp(runState)"; + } + @Override + public void dispose() { + super.dispose(); + } + }; + final RunStateListener runStateListener = new RunStateListener() { + @Override + public void stateChanged(ILaunchConfiguration changedConf) { + if (getLaunchConfigs().contains(changedConf)) { + exp.refresh(); + } + } + }; + tracker.addListener(runStateListener); + exp.onDispose(new DisposeListener() { + public void disposed(Disposable disposed) { + tracker.removeListener(runStateListener); + } + }); + addDisposableChild(exp); + exp.refresh(); + return exp; + } + + protected LiveExpression createActualInstancesExp() { + final LaunchConfRunStateTracker tracker = runStateTracker(); + final LiveExpression exp = new LiveExpression(0) { + @Override + public String toString() { + return "LiveExp(actualInstances)"; + } + protected Integer compute() { + int activeCount = 0; + for (ILaunchConfiguration c : getLaunchConfigs()) { + if (READY_STATES.contains(tracker.getState(c))) { + activeCount++; + } + } + return activeCount; + } + }; + final RunStateListener runStateListener = new RunStateListener() { + @Override + public void stateChanged(ILaunchConfiguration changedConf) { + if (getLaunchConfigs().contains(changedConf)) { + exp.refresh(); + } + } + }; + tracker.addListener(runStateListener); + exp.onDispose(new DisposeListener() { + public void disposed(Disposable disposed) { + tracker.removeListener(runStateListener); + } + }); + addDisposableChild(exp); + exp.refresh(); + return exp; + } + + protected LiveExpression createPortExpression(final LiveExpression runState) { + return createLivePortExp(runState, "local.server.port"); + } + + protected LiveExpression createActuatorPortExpression(final LiveExpression runState) { + return createLivePortExp(runState, "local.management.port"); + } + + private LiveExpression createLivePortExp(final LiveExpression runState, final String propName) { + AsyncLiveExpression exp = new AsyncLiveExpression(-1, "Refreshing port info ("+propName+") for "+getName()) { + { + //Doesn't really depend on runState, but should be recomputed when runState changes. + dependsOn(runState); + } + @Override + protected Integer compute() { + return getLivePort(propName); + } + @Override + public String toString() { + return "LivePortExp("+propName+")"; + } + }; + addDisposableChild(exp); + return exp; + } + + protected ActuatorClient getActuatorClient() { + return JMXActuatorClient.forPort(getTypeLookup(), this::getJmxPort); + } + + @Override + public Failable> getLiveRequestMappings() { + synchronized (this) { + if (liveRequestMappings==null) { + ActuatorClient client = getActuatorClient(); + liveRequestMappings = PollingLiveExp.create(Failable.error(MissingLiveInfoMessages.NOT_YET_COMPUTED), () -> { + List requestMappings = client.getRequestMappings(); + return requestMappings == null ? + Failable.error(getBootDashModel().getRunTarget().getType().getMissingLiveInfoMessages().getMissingInfoMessage(getName(), "mappings")) : + Failable.of(ImmutableList.copyOf(requestMappings)); + }); + addElementState(liveRequestMappings); + addDisposableChild(liveRequestMappings); + runState.addListener((e, runstate) -> { + if (READY_STATES.contains(runstate)) { + liveRequestMappings.refreshFor(LIVE_DATA_REFRESH_TIMEOUT); + } else { + liveRequestMappings.refreshOnce(); + } + }); + } + return liveRequestMappings.getValue(); + } + } + + public Failable getLiveBeans() { + synchronized (this) { + if (liveBeans == null) { + ActuatorClient client = getActuatorClient(); + liveBeans = PollingLiveExp.create(Failable.error(MissingLiveInfoMessages.NOT_YET_COMPUTED), () -> { + LiveBeansModel beans = client.getBeans(); + return beans == null ? Failable.error(getBootDashModel().getRunTarget().getType().getMissingLiveInfoMessages().getMissingInfoMessage(getName(), "beans")) : + Failable.of(beans); + }); + addElementState(liveBeans); + addDisposableChild(liveBeans); + runState.addListener((e, runstate) -> { + if (READY_STATES.contains(runstate)) { + // After the app is running refresh for 2 minutes every 5 sec + liveBeans.sleepBetweenRefreshes(Duration.ofSeconds(5)); + liveBeans.refreshFor(LIVE_DATA_REFRESH_TIMEOUT); + } else { + liveBeans.refreshOnce(); + } + }); + } + return liveBeans.getValue(); + } + } + + public Failable getLiveEnv() { + synchronized (this) { + if (liveEnv == null) { + ActuatorClient client = getActuatorClient(); + liveEnv = PollingLiveExp.create(Failable.error(MissingLiveInfoMessages.NOT_YET_COMPUTED), () -> { + LiveEnvModel env = client.getEnv(); + return env == null ? Failable.error(getBootDashModel().getRunTarget().getType().getMissingLiveInfoMessages().getMissingInfoMessage(getName(), "env")) : + Failable.of(env); + }); + addElementState(liveEnv); + addDisposableChild(liveEnv); + runState.addListener((e, runstate) -> { + if (READY_STATES.contains(runstate)) { + // After the app is running refresh for 2 minutes every 5 sec + liveEnv.sleepBetweenRefreshes(Duration.ofSeconds(5)); + liveEnv.refreshFor(LIVE_DATA_REFRESH_TIMEOUT); + } else { + liveEnv.refreshOnce(); + } + }); + } + return liveEnv.getValue(); + } + } + + private int getJmxPort() { + for (ILaunchConfiguration c : getLaunchConfigs()) { + for (ILaunch l : LaunchUtils.getLaunches(c)) { + if (!l.isTerminated()) { + int port = BootLaunchConfigurationDelegate.getJMXPortAsInt(l); + if (port>0) { + return port; + } + } + } + } + return -1; + } + + private int getLivePort(String propName) { + debug("["+this.getName()+"] getLivePort("+propName+")"); + ILaunchConfiguration conf = getActiveConfig(); + debug("["+this.getName()+"] getLivePort("+propName+") conf = "+conf); + if (conf!=null && READY_STATES.contains(getRunState())) { + debug("["+this.getName()+"] getLivePort("+propName+") runstate ok"); + if (BootLaunchConfigurationDelegate.canUseLifeCycle(conf) || CloudCliServiceLaunchConfigurationDelegate.canUseLifeCycle(conf)) { + debug("["+this.getName()+"] getLivePort("+propName+") canUseLifeCycle ok"); + //TODO: what if there are several launches? Right now we ignore all but the first + // non-terminated launch. + for (ILaunch l : BootLaunchUtils.getLaunches(conf)) { + if (!l.isTerminated()) { + debug("["+this.getName()+"] getLivePort("+propName+") found a launch"); + int jmxPort = BootLaunchConfigurationDelegate.getJMXPortAsInt(l); + debug("["+this.getName()+"] getLivePort("+propName+") jmxPort = "+jmxPort); + if (jmxPort>0) { + SpringApplicationLifeCycleClientManager cm = null; + try { + cm = new SpringApplicationLifeCycleClientManager(jmxPort); + SpringApplicationLifecycleClient c = cm.getLifeCycleClient(); + debug("["+this.getName()+"] getLivePort("+propName+") lifeCycleClient = "+c); + if (c!=null) { + //Just because lifecycle bean is ready does not mean that the port property has already been set. + //To avoid race condition we should wait here until the port is set (some apps aren't web apps and + //may never get a port set, so we shouldn't wait indefinitely!) + return RetryUtil.retry(100, 1000, () -> { + debug("["+this.getName()+"] getLivePort("+propName+") trying to get..."); + int port = c.getProperty(propName, -1); + debug("["+this.getName()+"] getLivePort("+propName+") port = "+ port); + if (port<=0) { + throw new IllegalStateException("port not (yet) set"); + } + return port; + }); + } + } catch (Exception e) { + debug(ExceptionUtil.getMessage(e)); + //most likely this just means the app isn't running so ignore + } finally { + if (cm!=null) { + cm.disposeClient(); + } + } + } + } + } + } + } + debug("["+this.getName()+"] getLivePort("+propName+") => -1"); + return -1; + } + + public void restartAndExpose(RunState runMode, NGROKClient ngrokClient, String eurekaInstance, UserInteractions ui) throws Exception { + String launchMode = null; + if (RunState.RUNNING.equals(runMode)) { + launchMode = ILaunchManager.RUN_MODE; + } + else if (RunState.DEBUGGING.equals(runMode)) { + launchMode = ILaunchManager.DEBUG_MODE; + } + else { + throw new IllegalArgumentException("Restart and expose expects RUNNING or DEBUGGING as 'goal' state"); + } + + int port = getLivePort(); + stopSync(); + + if (port <= 0) { + port = SocketUtil.findFreePort(); + } + + ILaunchConfiguration launchConfig = getOrCreateLaunchConfig(ui); + if (launchConfig != null) { + String tunnelName = launchConfig.getName(); + + NGROKTunnel tunnel = ngrokClient.startTunnel("http", Integer.toString(port)); + NGROKLaunchTracker.add(tunnelName, ngrokClient, tunnel); + + if (tunnel == null) { + ui.errorPopup("ngrok tunnel not started", "there was a problem starting the ngrok tunnel, try again or start a tunnel manually."); + return; + } + + String tunnelURL = tunnel.getPublic_url(); + + if (tunnelURL.startsWith("http://")) { + tunnelURL = tunnelURL.substring(7); + } + + Map extraAttributes = new HashMap<>(); + extraAttributes.put("spring.boot.prop.server.port", "1" + Integer.toString(port)); + extraAttributes.put("spring.boot.prop.eureka.instance.hostname", "1" + tunnelURL); + extraAttributes.put("spring.boot.prop.eureka.instance.nonSecurePort", "1" + "80"); + extraAttributes.put("spring.boot.prop.eureka.client.service-url.defaultZone", "1" + eurekaInstance); + + start(launchMode, launchConfig, extraAttributes); + } + } + + private void start(final String runMode, ILaunchConfiguration launchConfig, Map extraAttributes) { + try { + if (launchConfig != null) { + ILaunchConfigurationWorkingCopy workingCopy = launchConfig.getWorkingCopy(); + + removeOverriddenAttributes(workingCopy, extraAttributes); + addAdditionalAttributes(workingCopy, extraAttributes); + + launch(runMode, workingCopy); + } + } catch (Exception e) { + Log.log(e); + } + } + + private void addAdditionalAttributes(ILaunchConfigurationWorkingCopy workingCopy, Map extraAttributes) { + if (extraAttributes != null && extraAttributes.size() > 0) { + Iterator iterator = extraAttributes.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + String value = extraAttributes.get(key); + + workingCopy.setAttribute(key, value); + } + } + } + + private void removeOverriddenAttributes(ILaunchConfigurationWorkingCopy workingCopy, Map attributesToOverride) { + try { + Map attributes = workingCopy.getAttributes(); + Set keys = attributes.keySet(); + + Iterator iter = keys.iterator(); + while (iter.hasNext()) { + String existingKey = iter.next(); + if (containsSimilarKey(attributesToOverride, existingKey)) { + workingCopy.removeAttribute(existingKey); + } + } + } catch (CoreException e) { + e.printStackTrace(); + } + } + + private boolean containsSimilarKey(Map attributesToOverride, String existingKey) { + Iterator iter = attributesToOverride.keySet().iterator(); + while (iter.hasNext()) { + String overridingKey = iter.next(); + if (existingKey.startsWith(overridingKey)) { + return true; + } + } + return false; + } + + public void shutdownExpose() { + ImmutableSet launchConfigs = getLaunchConfigs(); + + for (ILaunchConfiguration launchConfig : launchConfigs) { + String tunnelName = launchConfig.getName(); + NGROKClient client = NGROKLaunchTracker.get(tunnelName); + + if (client != null) { + client.shutdown(); + NGROKLaunchTracker.remove(tunnelName); + } + } + } + + public void refreshLivePorts() { + refresh(livePort, actuatorPort); + } + + private void refresh(LiveExpression... exps) { + for (LiveExpression e : exps) { + if (e!=null) { + e.refresh(); + } + } + } + + @Override + public LocalBootDashModel getBootDashModel() { + return (LocalBootDashModel) super.getBootDashModel(); + } + + @Override + public EnumSet supportedGoalStates() { + return RunTargets.LOCAL_RUN_GOAL_STATES; + } + + @Override + public LocalRunTarget getTarget() { + return (LocalRunTarget) super.getTarget(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractRunTarget.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractRunTarget.java new file mode 100644 index 000000000..b0ef8cd1d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractRunTarget.java @@ -0,0 +1,182 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.NAME; +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.RUN_STATE_ICN; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.resource.ImageDescriptor; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTarget; +import org.springframework.ide.eclipse.boot.dash.util.template.Template; +import org.springframework.ide.eclipse.boot.dash.util.template.TemplateEnv; +import org.springframework.ide.eclipse.boot.dash.util.template.Templates; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; + +public abstract class AbstractRunTarget extends AbstractDisposable implements RunTarget, TemplateEnv { + + private static final String NAME_TEMPLATE = "NAME_TEMPLATE"; + + private static final BootDashColumn[] DEFAULT_COLS = { + RUN_STATE_ICN, + NAME, + }; + + private String id; + private String name; + private RunTargetType type; + private IPropertyStore propertyStore; + + public AbstractRunTarget(RunTargetType type, String id, String name) { + Assert.isNotNull(name); + Assert.isNotNull(id); + Assert.isNotNull(type); + this.id = id; + this.name = name; + this.type = type; + IPropertyStore typeStore = type.getPropertyStore(); + if (typeStore!=null) { + propertyStore = PropertyStores.createSubStore(id, typeStore); + } + } + + public AbstractRunTarget(RunTargetType type, String idAndName) { + this(type, idAndName, idAndName); + } + + @Override + public String getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public String toString() { + return "RunTarget("+getType().getName()+", "+id+")"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AbstractRunTarget other = (AbstractRunTarget) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; + return true; + } + + @Override + public RunTargetType getType() { + return type; + } + + @Override + public String getNameTemplate() { + PropertyStoreApi props = getPersistentProperties(); + if (props!=null) { + String localTemplate = props.get(NAME_TEMPLATE); + if (localTemplate!=null) { + return localTemplate; + } + } + return getType().getNameTemplate(); + } + + @Override + public void setNameTemplate(String template) throws Exception { + getPersistentProperties().put(NAME_TEMPLATE, template); + } + + @Override + public String getDisplayName() { + Template template = Templates.create(getNameTemplate()); + if (template!=null) { + return template.render(this); + } + return getName(); + } + + @Override + public String getTemplateVar(char name) { + return null; + } + + @Override + public boolean hasCustomNameTemplate() { + PropertyStoreApi props = getPersistentProperties(); + if (props!=null) { + return props.get(NAME_TEMPLATE)!=null; + } + return false; + } + + @Override + public IPropertyStore getPropertyStore() { + return propertyStore; + } + + @Override + public PropertyStoreApi getPersistentProperties() { + IPropertyStore store = getPropertyStore(); + if (store!=null) { + return new PropertyStoreApi(store); + } + return null; + } + + @Override + public BootDashColumn[] getDefaultColumns() { + return DEFAULT_COLS; + } + + @Override + public final ImageDescriptor getIcon() { + if (this instanceof RemoteRunTarget) { + if (((RemoteRunTarget)this).isConnected()) { + return getType().getIcon(); + } else { + return getType().getDisconnectedIcon(); + } + } + return getType().getIcon(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AsyncDeletable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AsyncDeletable.java new file mode 100644 index 000000000..74f0c70d3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AsyncDeletable.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.model; + +import reactor.core.publisher.Mono; + +/** + * @author Kris De Volder + */ +public interface AsyncDeletable { + + Mono deleteAsync(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashElement.java new file mode 100644 index 000000000..7e2d60fe0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashElement.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.model; + +import java.util.EnumSet; + +import org.eclipse.core.resources.IProject; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansModel; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.LiveEnvModel; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +public interface BootDashElement extends App, Taggable { + IJavaProject getJavaProject(); + IProject getProject(); + RunState getRunState(); + + + /** + * Return the port this element is running on. If the port can not + * be determined or the app is not running this returns -1. + *

+ * Deprecated: should use getLivePorts. + */ + @Deprecated + int getLivePort(); + + /** + * @return The host the app is running on. May return null if + * app is not running or host is not known. + */ + String getLiveHost(); + + + /** + * Get the request mappings from a running process. May return null if + * request mappings can not be determined. (So 'null' means 'unknown', whereas + * an empty list means 'no request mappings'). + */ + Failable> getLiveRequestMappings(); + + /** + * Get the beans from a running process. May return null if beans cannot be + * determined. (Thus, null means unknown, whereas an empty list + * means 'no beans') + */ + Failable getLiveBeans(); + + /** + * Get the env from a running process. May return null if env cannot be + * determined. (Thus, null means unknown, whereas an empty list + * means 'no env') + */ + Failable getLiveEnv(); + + /** + * Get the 'active' launch configuration. This may be null. + *

+ * If only one existing configuration is associated with this element then + * it is automatically considered as the 'active' configuration. + *

+ * If there are no configurations associated with this element then the active configuration + * is undefined (null). + *

+ * If more than one configuration exists then the 'preferred config' is used to decide which one + * of the existing elements should be considered as 'active'. + * + * TODO: isn't this supposed to be obsolete? Remove? + * + * @return active configuration or null. + */ + ILaunchConfiguration getActiveConfig(); + + /** + * The 'default' path is used by some actions to quickly open + * the app in a browser view. This is just a stored value. There is no guarantee + * that it actually exists on the given element when it is running (i.e. it may + * or may not be the path of a RequestMapping returned from getLiveRequestMappings. + */ + String getDefaultRequestMappingPath(); + void setDefaultRequestMappingPath(String defaultPath); + + BootDashModel getBootDashModel(); + + void stop() throws Exception; + void restart(RunState runingOrDebugging, UserInteractions ui) throws Exception; + void openConfig(UserInteractions ui); + int getActualInstances(); + int getDesiredInstances(); + + ImmutableSet getCurrentChildren(); + ObservableSet getChildren(); + ImmutableSet getLaunchConfigs(); + ImmutableSet getLivePorts(); + + /** + * Fetch the parent of a BDE. If this is a nested BDE then the parent will be + * another {@link BootDashElement}. If the element is one owned directly by a + * {@link BootDashModel} then the parent is that model. + */ + Object getParent(); + BootDashColumn[] getColumns(); + boolean projectHasDevtoolsDependency(); + + String getUrl(); + + /** + * @return Subset of the runstate that a user can request when changing a + * DashBoardElement's 'run-state'. Essentially, this allows + * determining whether a given BootDahsElement can support the + * 'stop', 'run' and 'debug' operations which request that the + * element be brought into a given run-state. + */ + EnumSet supportedGoalStates(); + + default ImageDescriptor getRunStateImageDecoration() { return null; } + default ImageDescriptor getCustomRunStateIcon() { return null; } + default Image getPropertiesTitleIconImage() { return null; } + default String getProtocol() { return "http"; } + default boolean isDevtoolsGreenColor() { return projectHasDevtoolsDependency(); } + default RefreshState getRefreshState() { return RefreshState.READY; } + default boolean hasDevtoolsDependency() { return projectHasDevtoolsDependency(); } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashElementSearchFilter.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashElementSearchFilter.java new file mode 100644 index 000000000..ad333b6eb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashElementSearchFilter.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2015 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.model; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.ui.IWorkingSet; +import org.eclipse.ui.PlatformUI; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; + +/** + * Boot Dash elements filter working on strings from BDEs tags combined with + * working set names (if BDE element delegates to a project in the workspace + * + * @author Alex Boyko + * + */ +public class BootDashElementSearchFilter extends TagSearchFilter { + + private Map> mapping; + + public BootDashElementSearchFilter(String s) { + super(s); + } + + private Map> getMapping() { + if (mapping == null) { + mapping = new HashMap<>(); + for (IWorkingSet ws : PlatformUI.getWorkbench().getWorkingSetManager().getAllWorkingSets()) { + if (!ws.isAggregateWorkingSet()) { + for (IAdaptable a : ws.getElements()) { + IProject project = (IProject)a.getAdapter(IProject.class); + if (project != null) { + Set set = mapping.get(project); + if (set == null) { + set = new HashSet<>(); + mapping.put(project, set); + } + set.add(ws.getName()); + } + } + } + } + } + return mapping; + } + + @Override + protected ImmutableSet getTags(BootDashElement element) { + Builder tags = ImmutableSet.builder(); + tags.addAll(super.getTags(element)); + // Add implicit tag for element name + tags.add(element.getName()); + // Add implicit tags for Working Sets + if (element.getProject() != null) { + Set workingSetNames = getMapping().get(element.getProject()); + if (workingSetNames != null) { + tags.addAll(workingSetNames); + } + } + return tags.build(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashElementsFilterBoxModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashElementsFilterBoxModel.java new file mode 100644 index 000000000..aca89685b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashElementsFilterBoxModel.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2015 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.model; + +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.core.FilterBoxModel; +import org.springsource.ide.eclipse.commons.livexp.util.Filter; +import org.springsource.ide.eclipse.commons.livexp.util.Filters; + +/** + * @author Kris De Volder + */ +public class BootDashElementsFilterBoxModel extends FilterBoxModel { + + @Override + protected Filter createFilterForInput(String text) { + if (StringUtil.hasText(text)) { + return new BootDashElementSearchFilter(text); + } else { + return Filters.acceptAll(); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashHyperlink.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashHyperlink.java new file mode 100644 index 000000000..9fe463c1c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashHyperlink.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://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.concurrent.atomic.AtomicBoolean; + +public abstract class BootDashHyperlink implements ButtonModel { + + private String linkText; + + private AtomicBoolean busy = new AtomicBoolean(false); + + public BootDashHyperlink(String linkText) { + this.linkText = linkText; + } + + /** + * This overrides/implements {@link ButtonModel} with some stuff to ensure that + * only a single trigger of the button is active at the same time, and executes + * the button action in a Job. + *

+ * Subclasses should override doPerform instead. + */ + final synchronized public void perform(UserInteractions ui) throws Exception { + if (busy.compareAndSet(false, true)) { + try { + doPerform(ui); + } finally { + busy.set(false); + } + } + } + + protected abstract void doPerform(UserInteractions ui) throws Exception; + + @Override + public String getLabel() { + return linkText; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashModel.java new file mode 100644 index 000000000..04d0ef6de --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashModel.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 2020 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.model; + +import java.util.Collection; +import java.util.Comparator; + +import org.springframework.ide.eclipse.boot.dash.views.BootDashModelConsoleManager; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +public interface BootDashModel { + + interface ModelStateListener { + void stateChanged(BootDashModel model); + } + + interface ElementStateListener { + /** + * Called when something about the element has changed. + *

+ * Note this doesn't get called when (top-level) elements are + * added / removed to the model. Only when some property of + * the element itself has changed. + *

+ * Note: think of the 'children' of an element as a propery of its parent element. + * So, if a child is added/removed to/from an element then the element + * itself will receive a stateChanged event. + */ + void stateChanged(BootDashElement e); + } + + RunTarget getRunTarget(); + + ObservableSet getElements(); + + BootDashModelConsoleManager getElementConsoleManager(); + + /** + * 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. + */ + void dispose(); + + /** + * Trigger manual model refresh. + */ + void refresh(UserInteractions ui); + + void addElementStateListener(BootDashModel.ElementStateListener l); + + void removeElementStateListener(BootDashModel.ElementStateListener l); + + void addModelStateListener(BootDashModel.ModelStateListener l); + + void removeModelStateListener(BootDashModel.ModelStateListener l); + + BootDashViewModel getViewModel(); + + /** + * Send notification to listenters that a given element's state changed. + * + * @param element The element that changed + * @param info Some additional info, more or less reflects the 'reason' for the change event. + * This can be used to be to log/track the cause of a change event. + * This can be very useful to debug test failures. It is not really intended to be + * used for any other purpose. + */ + void notifyElementChanged(BootDashElement element, Object info); + + default RefreshState getRefreshState() { return RefreshState.READY; }; + + Comparator getElementComparator(); + + void notifyModelStateChanged(); + + /** + * Gets the current name template associated with this model. This may either be + * a custom template set via the 'setNameTemplate' method, or it might be a + * template inherited from the runtarget type, or it may be null (if the runtarget + * type does not provide a name template. + * + * @return The effective name template or null. + */ + String getNameTemplate(); + + /** + * Set a custom name template for this model. Note that this only works on models who's target provides support for + * persistent properties (since that's where this value is ultimately stored). + *

+ * Setting the template to null makes the effective template be inherited from the runtarget type. + */ + void setNameTemplate(String template) throws Exception; + + /** + * @return true if this model has a custom name template (false means it inherits name template from its target type). + */ + boolean hasCustomNameTemplate(); + + String getDisplayName(); + + ObservableSet getButtons(); + + void performDoubleClickAction(UserInteractions ui); + + default BootDashElement getApplication(String appName) { + return findNameIn(appName, getElements().getValues()); + } + + static BootDashElement findNameIn(String appName, Collection children) { + for (BootDashElement bde : children) { + if (appName.equals(bde.getName())) { + return bde; + } + BootDashElement found = findNameIn(appName, bde.getChildren().getValues()); + if (found!=null) { + return found; + } + } + return null; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashModelContext.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashModelContext.java new file mode 100644 index 000000000..1a16698fd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashModelContext.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import java.util.regex.Pattern; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.runtime.IPath; +import org.eclipse.debug.core.ILaunchManager; +import org.springframework.ide.eclipse.boot.core.cli.BootInstallManager; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.IScopedPropertyStore; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +/** + * @author Kris De Volder + */ +public abstract class BootDashModelContext { + + public final SimpleDIContext injections; + + //TODO: many places where this being passed around it is accompanied by a BootDashViewModel. + // These two parameters passed together represent the real 'BootDashModelContext'. + //So the proper thing to do is: + // + // - rename this interface to BootDashViewModelContext (it represents the context of the viewmodel not of the indivual sections within + // - create a new class or interface called BootDashModelContext which contains + // - a BootDashModelContext + // - a BootDashViewModel + // - where both of these types occur together, replace with a reference to the new BootDashViewModelContext + + public abstract IWorkspace getWorkspace(); + + public abstract ILaunchManager getLaunchManager(); + + public abstract IPath getStateLocation(); + + public abstract IScopedPropertyStore getProjectProperties(); + public abstract IScopedPropertyStore getRunTargetProperties(); + public abstract IPropertyStore getViewProperties(); + + public abstract SecuredCredentialsStore getSecuredCredentialsStore(); + + /** + * A store for properties which is suitable for sensitive data with basic protection. + * I.e. backed by a unencrypted file which is made only accessible to the current user + * and protected by the os. The file is not encrypted in any way. + */ + public abstract IPropertyStore getPrivatePropertyStore(); + + public abstract void log(Exception e); + + /** + *!!!!Warning!!!!

+ * This is 'injected' but in fact the injected filter is only used for + * triggering refreshes. It is not actually used to filter projects. + * Projects are filtered indirectly via static method BootPropertyTester.isBootProject. + * that means that injecting a different filter than the one used by that method + * will not work as expected. + */ + public abstract LiveExpression getBootProjectExclusion(); + + public abstract BootInstallManager getBootInstallManager(); + + public final UserInteractions getUi() { + return injections.getBean(UserInteractions.class); + } + + protected BootDashModelContext(SimpleDIContext injections) { + this.injections = injections; + injections.defInstance(BootDashModelContext.class, this); + injections.assertDefinitionFor(UserInteractions.class); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashModelManager.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashModelManager.java new file mode 100644 index 000000000..0acea0ddb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashModelManager.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * Copyright (c) 2015 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.model; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.ListenerList; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSet; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; +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 com.google.common.collect.ImmutableSet; + +/** + * An instance of this class manages a LiveSet of BootDashModels, one model per + * RunTarget. New models are created or disposed to keep them in synch with the + * RunTargets. + * + * @author Nieraj Singh + * @author Kris De Volder + */ +public class BootDashModelManager implements Disposable { + + private LiveSetVariable models; + private Map modelsPerTargetId; + private ObservableSet targets; + private RunTargetChangeListener targetListener; + private ListenerList elementStateListeners = new ListenerList(); + private ElementStateListener upstreamElementStateListener; + + private BootDashModelContext context; + private BootDashViewModel viewModel; + + public BootDashModelManager(BootDashModelContext context, BootDashViewModel viewModel, ObservableSet targets) { + this.context = context; + this.viewModel = viewModel; + this.targets = targets; + } + + public LiveExpression> getModels() { + if (models == null) { + models = new LiveSetVariable<>(new LinkedHashSet()); + modelsPerTargetId = new LinkedHashMap<>(); + targets.addListener(targetListener = new RunTargetChangeListener()); + } + return models; + } + + class RunTargetChangeListener implements ValueListener> { + + @Override + public void gotValue(LiveExpression> exp, ImmutableSet actualRunTargets) { + synchronized (modelsPerTargetId) { + Map currentTargetsPerId = new LinkedHashMap<>(); + if (actualRunTargets != null) { + for (RunTarget runTarget : actualRunTargets) { + String id = runTarget.getId(); + Assert.isNotNull(id); + currentTargetsPerId.put(id, runTarget); + } + } + + // To avoid firing unnecessary model change events, only modify + // list of models IF there is a change + boolean hasChanged = false; + + // Add models for new targets + Set modelsToKeep = new LinkedHashSet<>(); + + for (Entry entry : currentTargetsPerId.entrySet()) { + if (modelsPerTargetId.get(entry.getKey()) == null) { + BootDashModel model = entry.getValue().createSectionModel(viewModel); + if (model != null) { + modelsPerTargetId.put(entry.getKey(), model); + hasChanged = true; + } + } + + if (modelsPerTargetId.get(entry.getKey()) != null) { + modelsToKeep.add(modelsPerTargetId.get(entry.getKey())); + } + } + + // Remove models for deleted targets + for (Iterator> it = modelsPerTargetId.entrySet().iterator(); it + .hasNext();) { + Entry entry = it.next(); + if (currentTargetsPerId.get(entry.getKey()) == null) { + it.remove(); + hasChanged = true; + } + } + + if (hasChanged) { + models.replaceAll(modelsToKeep); + } + } + } + } + + public void dispose() { + if (targetListener != null) { + targets.removeListener(targetListener); + targetListener = null; + } + if (models != null) { + for (BootDashModel m : models.getValues()) { + if (upstreamElementStateListener != null) { + m.removeElementStateListener(upstreamElementStateListener); + } + m.dispose(); + } + upstreamElementStateListener = null; + models = null; + } + } + + public void addElementStateListener(ElementStateListener l) { + elementStateListeners.add(l); + ensureUpstreamStateListener(); + } + + private synchronized void ensureUpstreamStateListener() { + if (upstreamElementStateListener == null) { + upstreamElementStateListener = new ElementStateListener() { + public void stateChanged(BootDashElement e) { + for (Object o : elementStateListeners.getListeners()) { + ((ElementStateListener) o).stateChanged(e); + } + } + }; + getModels().addListener(new ValueListener>() { + public void gotValue(LiveExpression> exp, ImmutableSet models) { + for (BootDashModel m : models) { + m.addElementStateListener(upstreamElementStateListener); + } + } + }); + } + } + + public void removeElementStateListener(ElementStateListener l) { + elementStateListeners.remove(l); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashViewModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashViewModel.java new file mode 100644 index 000000000..32d2474e9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootDashViewModel.java @@ -0,0 +1,181 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.model; + +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsUtil; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springframework.ide.eclipse.boot.dash.util.TreeAwareFilter; +import org.springframework.ide.eclipse.boot.util.ProcessTracker; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +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.ui.Disposable; +import org.springsource.ide.eclipse.commons.livexp.util.Filter; +import org.springsource.ide.eclipse.commons.livexp.util.Filters; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * @author Kris De Volder + */ +public class BootDashViewModel extends AbstractDisposable { + + private LiveSetVariable runTargets; + private BootDashModelManager models; + private Set runTargetTypes; + private RunTargetPropertiesManager manager; + private ToggleFiltersModel toggleFiltersModel; + private BootDashElementsFilterBoxModel filterBox; + private LiveExpression> filter; + private List orderedRunTargetTypes; + private Comparator modelComparator; + private Comparator targetComparator; + private BootDashModelContext context; + + /** + * Create an 'empty' BootDashViewModel with no run targets. Targets can be + * added by adding them to the runTarget's LiveSet. + */ + public BootDashViewModel(SimpleDIContext injections) + { + context = injections.getBean(BootDashModelContext.class); + List runTargetTypes = context.injections.getBeans(RunTargetType.class); + runTargets = new LiveSetVariable<>(new LinkedHashSet(), AsyncMode.SYNC); + models = new BootDashModelManager(context, this, runTargets); + + manager = new RunTargetPropertiesManager(context, runTargetTypes); + List existingtargets = manager.load(); + runTargets.addAll(existingtargets); + runTargets.addListener(manager); + + this.orderedRunTargetTypes = runTargetTypes; + this.targetComparator = new RunTargetComparator(orderedRunTargetTypes); + this.modelComparator = new BootModelComparator(targetComparator); + + this.runTargetTypes = new LinkedHashSet<>(orderedRunTargetTypes); + filterBox = new BootDashElementsFilterBoxModel(); + toggleFiltersModel = new ToggleFiltersModel(context); + LiveExpression> baseFilter = filterBox.getFilter(); + LiveExpression> treeAwarefilter = baseFilter.apply(new Function, Filter>() { + public Filter apply(Filter input) { + return new TreeAwareFilter(input); + } + }); + filter = Filters.compose(treeAwarefilter, toggleFiltersModel.getFilter()); + addDisposableChild(filter); + addDisposableChild(DevtoolsUtil.createProcessTracker(this)); + } + + public LiveSetVariable getRunTargets() { + return runTargets; + } + + @Override + public void dispose() { + for (RunTargetType rtt : runTargetTypes) { + if (rtt instanceof Disposable) { + ((Disposable) rtt).dispose(); + } + } + models.dispose(); + filterBox.dispose(); + super.dispose(); + } + + public void addElementStateListener(ElementStateListener l) { + models.addElementStateListener(l); + } + + public void removeElementStateListener(ElementStateListener l) { + models.removeElementStateListener(l); + } + + public LiveExpression> getSectionModels() { + return models.getModels(); + } + + public Set getRunTargetTypes() { + return runTargetTypes; + } + + public void removeTarget(RunTarget toRemove, UserInteractions userInteraction) { + if (toRemove != null) { + String name = toRemove.getName(); + boolean found = runTargets.getValues().contains(toRemove); + if (found && userInteraction.confirmOperation("Deleting run target: " + name, + "Are you sure that you want to delete " + name + + "? All information regarding this target will be permanently removed.")) { + runTargets.remove(toRemove); + toRemove.dispose(); + } + } + } + + public void updateTargetPropertiesInStore() { + manager.store(getRunTargets().getValue()); + } + + public ToggleFiltersModel getToggleFilters() { + return toggleFiltersModel; + } + + public BootDashElementsFilterBoxModel getFilterBox() { + return filterBox; + } + + public LiveExpression> getFilter() { + return filter; + } + + public RunTarget getRunTargetById(String targetId) { + for (BootDashModel m : getSectionModels().getValue()) { + RunTarget target = m.getRunTarget(); + if (target.getId().equals(targetId)) { + return target; + } + }; + return null; + } + + public BootDashModel getSectionByTargetId(String targetId) { + for (BootDashModel m : getSectionModels().getValue()) { + if (m.getRunTarget().getId().equals(targetId)) { + return m; + } + }; + return null; + } + + public Comparator getModelComparator() { + return this.modelComparator; + } + + public Comparator getTargetComparator() { + return this.targetComparator; + } + + public BootDashModelContext getContext() { + return context; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootModelComparator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootModelComparator.java new file mode 100644 index 000000000..fbb06b987 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootModelComparator.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2015 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.model; + +import java.util.Comparator; + +/** + * Model is just a wrapper on a {@link RunTarget} so {@link BootModelComparator} should + * just use {@link RunTarget} comparator. + */ +public class BootModelComparator implements Comparator { + + private Comparator targetComparator; + + public BootModelComparator(Comparator targetComparator) { + this.targetComparator = targetComparator; + } + + public int compare(BootDashModel model1, BootDashModel model2) { + RunTarget t1 = model1.getRunTarget(); + RunTarget t2 = model2.getRunTarget(); + return targetComparator.compare(t1, t2); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootProjectDashElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootProjectDashElement.java new file mode 100644 index 000000000..b37595789 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootProjectDashElement.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.model; + +import org.eclipse.core.resources.IProject; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveDataCapableElement; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveDataConnectionManagementActions.ExecuteCommandAction; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.util.CollectionUtils; +import org.springframework.ide.eclipse.boot.dash.util.DebugUtil; +import org.springframework.ide.eclipse.boot.launch.util.BootLaunchUtils; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.IScopedPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; +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.LiveSets; +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 com.google.common.base.Function; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; + +/** + * Concrete BootDashElement that wraps an IProject + * + * @author Kris De Volder + */ +public class BootProjectDashElement extends AbstractLaunchConfigurationsDashElement implements BootDashElement, LiveDataCapableElement { + + private static final boolean DEBUG = DebugUtil.isDevelopment(); + + private static void debug(String string) { + if (DEBUG) { + System.err.println(string); + } + } + + private IScopedPropertyStore projectProperties; + + private LaunchConfDashElementFactory childFactory; + private ObservableSet children; + private ObservableSet ports; + + public BootProjectDashElement(IProject project, LocalBootDashModel context, IScopedPropertyStore projectProperties, + BootProjectDashElementFactory factory, LaunchConfDashElementFactory childFactory) { + super(context, project); + this.projectProperties = projectProperties; + this.childFactory = childFactory; +// if (DEBUG) { +// onDispose((e) -> { +// debug("disposed: "+this); +// }); +// } + } + + + @Override + public IProject getProject() { + return delegate; + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + protected IPropertyStore createPropertyStore() { + return PropertyStores.createForScope(delegate, projectProperties); + } + + @Override + public ImmutableSet getLaunchConfigs() { + return getLaunchConfigsExp().getValues(); + } + + @Override + public int getLivePort() { + return CollectionUtils.getAnyOr(getLivePorts(), -1); + } + + @Override + public ImmutableSet getLivePorts() { + return getLivePortsExp().getValues(); + } + + private ObservableSet getLivePortsExp() { + if (ports==null) { + ports = createSortedLiveSummary((BootDashElement element) -> { + int port = element.getLivePort(); + if (port>0) { + return port; + } + return null; + }); + this.dependsOn(ports); + } + return ports; + } + + /** + * Creates a ObservableSet that is a 'summary' of some property of the children of this node. The summary + * is a set of all the values of the given property on all the children. The set is sorted using the element's + * natural ordering. + */ + private > ObservableSet createSortedLiveSummary(final Function getter) { + + final ObservableSet summary = new ObservableSet() { + + @Override + protected ImmutableSet compute() { + debug("port-summary["+getName()+"]: compute()..."); + ImmutableSet.Builder builder = ImmutableSortedSet.naturalOrder(); + for (BootDashElement child : getCurrentChildren()) { + add(builder, child); + } + ImmutableSet result = builder.build(); + debug("port-summary["+getName()+"]: compute() => "+result); + return result; + } + + protected void add(ImmutableSet.Builder builder, BootDashElement child) { + debug("port-summary["+getName()+"]: add port for "+child); + T v = getter.apply(child); + debug("port-summary["+getName()+"]: add port for "+child+" = "+v); + if (v!=null) { + builder.add(v); + } + } + }; + final ElementStateListener elementListener = new ElementStateListener() { + public void stateChanged(BootDashElement e) { + summary.refresh(); + } + }; + getBootDashModel().addElementStateListener(elementListener); + summary.onDispose(new DisposeListener() { + public void disposed(Disposable disposed) { + getBootDashModel().removeElementStateListener(elementListener); + } + }); + addDisposableChild(summary); + return summary; + } + + private ObservableSet getLaunchConfigsExp() { + return getBootDashModel().launchConfTracker.getConfigs(delegate); + } + + /** + * All children including 'invisible ones' that may be hidden from the children returned + * by getChildren. + */ + @Override + public ObservableSet getChildren() { + if (children==null) { + children = LiveSets.mapSync(getBootDashModel().launchConfTracker.getConfigs(delegate), + new Function() { + public BootDashElement apply(ILaunchConfiguration input) { + return childFactory.createOrGet(input); + } + } + ); + children.addListener(new ValueListener>() { + public void gotValue(LiveExpression> exp, ImmutableSet value) { + getBootDashModel().notifyElementChanged(BootProjectDashElement.this, "children changed"); + refreshRunState(); + } + }); + addDisposableChild(children); + } + return this.children; + } + + @Override + public void refreshLivePorts() { + for (BootDashElement child : getChildren().getValues()) { + try { + ((AbstractLaunchConfigurationsDashElement)child).refreshLivePorts(); + } catch (ClassCastException e) { + //Should be impossible (unless something changes in how elements in dash are nested) + Log.log(e); + } + } + } + + @Override + public ImmutableSet getLaunches() { + return ImmutableSet.copyOf(BootLaunchUtils.getLaunches(getLaunchConfigs())); + } + + @Override + public Object getParent() { + return getBootDashModel(); + } + + @Override + public boolean matchesLiveProcessCommand(ExecuteCommandAction action) { + IProject project = getProject(); + return project!=null && project.getName().equals(action.getProjectName()); + } + + @Override + public Image getPropertiesTitleIconImage() { + return BootDashActivator.getDefault().getImageRegistry().get(BootDashActivator.BOOT_ICON); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootProjectDashElementFactory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootProjectDashElementFactory.java new file mode 100644 index 000000000..ba8c583dd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/BootProjectDashElementFactory.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * 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 + * https://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.Iterator; +import java.util.Map; +import java.util.Set; + +import org.eclipse.core.resources.IProject; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springsource.ide.eclipse.commons.core.pstore.IScopedPropertyStore; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.MapMaker; + +/** + * Manages the creating of BootProjectDashElement. It keeps track of all the created instances and + * ensures that if an element represents the same entity then the instance is reused. + * + * @author Kris De Volder + */ +public class BootProjectDashElementFactory implements Disposable { + + private LaunchConfDashElementFactory launchConfElementFactory; + private LocalBootDashModel model; + private IScopedPropertyStore projectProperties; + + private Map cache; + + public BootProjectDashElementFactory(LocalBootDashModel model, IScopedPropertyStore projectProperties, LaunchConfDashElementFactory launchConfElementFactory) { + this.cache = new MapMaker() + .concurrencyLevel(1) //single thread only so don't waste space for 'connurrencyLevel' support + .makeMap(); + this.model = model; + this.launchConfElementFactory = launchConfElementFactory; + this.projectProperties = projectProperties; + } + + public BootProjectDashElement createOrGet(IProject p) { + if (BootPropertyTester.workaroundMavenBundleInitializationIssue(p)) { + return null; + } + + if (BootPropertyTester.isBootProject(p)) { + BootProjectDashElement el; + synchronized (this) { + el = cache.get(p); + if (el==null) { + cache.put(p, el = new BootProjectDashElement(p, model, projectProperties, this, launchConfElementFactory)); + } + } + return el; + } + return null; + } + + public void dispose() { + disposeAllExcept(ImmutableSet.of()); + cache = null; + } + + /** + * Clients should call this to allow factory to remove/dispose elements that are no longer interesting. + */ + public synchronized void disposeAllExcept(Set toRetain) { + if (cache!=null) { + Iterator iter = cache.values().iterator(); + while (iter.hasNext()) { + BootProjectDashElement element = iter.next(); + if (!toRetain.contains(element)) { + iter.remove(); + element.dispose(); + } + } + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/ButtonModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/ButtonModel.java new file mode 100644 index 000000000..24141fa16 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/ButtonModel.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import org.springframework.ide.eclipse.boot.dash.util.Performable; + +/** + * Simple model element for something that behaves like a clickable button. + */ +public interface ButtonModel extends Performable { + String getLabel(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DefaultBootDashModelContext.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DefaultBootDashModelContext.java new file mode 100644 index 000000000..f650b965f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DefaultBootDashModelContext.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import java.util.regex.Pattern; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchManager; +import org.springframework.ide.eclipse.boot.core.BootPreferences; +import org.springframework.ide.eclipse.boot.core.cli.BootInstallManager; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.console.CloudAppLogManager; +import org.springframework.ide.eclipse.boot.dash.di.EclipseBeanLoader; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.metadata.PropertyStoreFactory; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElementDataContributor; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springframework.ide.eclipse.boot.dash.views.DefaultUserInteractions; +import org.springsource.ide.eclipse.commons.boot.ls.remoteapps.RemoteBootAppsDataHolder; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.IScopedPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.ui.UIContext; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * @author Kris De Volder + */ +public class DefaultBootDashModelContext extends BootDashModelContext { + + private IScopedPropertyStore projectProperties = PropertyStores.createForProjects(); + + private IScopedPropertyStore runTargetProperties = PropertyStoreFactory.createForRunTargets(); + + private SecuredCredentialsStore securedStore = PropertyStoreFactory.createSecuredCredentialsStore(); + + private IPropertyStore viewProperties = PropertyStores.backedBy(BootDashActivator.getDefault().getPreferenceStore()); + + private IPropertyStore privateProperties = PropertyStores.createPrivateStore(BootDashActivator.getDefault().getStateLocation().append("private.properties")); + + private BootInstallManager bootInstalls = BootInstallManager.getInstance(); + + public static BootDashModelContext create() { + SimpleDIContext injections = new SimpleDIContext(); + injections.defInstance(UIContext.class, UIContext.DEFAULT); + injections.defInstance(UserInteractions.class, new DefaultUserInteractions(injections)); + injections.def(BootDashViewModel.class, BootDashViewModel::new); + injections.def(BootDashModelContext.class, DefaultBootDashModelContext::new); + injections.defInstance(RunTargetType.class, RunTargetTypes.LOCAL); + injections.def(CloudAppLogManager.class, CloudAppLogManager::new); + injections.def(RemoteBootAppsDataHolder.Contributor.class, GenericRemoteAppElementDataContributor::new); + new EclipseBeanLoader(injections).loadFromExtensionPoint(BootDashActivator.INJECTIONS_EXTENSION_ID); + return new DefaultBootDashModelContext(injections); + } + + private DefaultBootDashModelContext(SimpleDIContext injections) { + super(injections); + } + + @Override + public IWorkspace getWorkspace() { + return ResourcesPlugin.getWorkspace(); + } + + @Override + public IPath getStateLocation() { + return BootDashActivator.getDefault().getStateLocation(); + } + + @Override + public ILaunchManager getLaunchManager() { + return DebugPlugin.getDefault().getLaunchManager(); + } + + @Override + public IScopedPropertyStore getProjectProperties() { + return projectProperties; + } + + @Override + public IScopedPropertyStore getRunTargetProperties() { + return runTargetProperties; + } + + @Override + public SecuredCredentialsStore getSecuredCredentialsStore() { + return securedStore; + } + + @Override + public void log(Exception e) { + Log.log(e); + } + + @Override + public LiveExpression getBootProjectExclusion() { + return BootPreferences.getInstance().getProjectExclusionExp(); + } + + @Override + public IPropertyStore getViewProperties() { + return viewProperties; + } + + @Override + public IPropertyStore getPrivatePropertyStore() { + return privateProperties; + } + + @Override + public BootInstallManager getBootInstallManager() { + return bootInstalls; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DefaultSecuredCredentialsStore.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DefaultSecuredCredentialsStore.java new file mode 100644 index 000000000..4dc35abd8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DefaultSecuredCredentialsStore.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.model; + +import org.eclipse.equinox.security.storage.EncodingUtils; +import org.eclipse.equinox.security.storage.ISecurePreferences; +import org.eclipse.equinox.security.storage.SecurePreferencesFactory; +import org.eclipse.equinox.security.storage.StorageException; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; + +/** + * Secured storage for {@link RunTarget} passwords. + * + */ +public class DefaultSecuredCredentialsStore implements SecuredCredentialsStore { + + private static final String KEY_PASSWORD = "password"; + + private boolean isUnlocked = false; + + public DefaultSecuredCredentialsStore() { + } + + public void remove(String runTargetId) { + ISecurePreferences preferences = getSecurePreferences(runTargetId); + if (preferences != null) { + preferences.removeNode(); + } + } + + @Override + public String getCredentials(String runTargetId) throws StorageException { + return readProperty(KEY_PASSWORD, runTargetId); + } + + @Override + public void setCredentials(String runTargetId, String password) throws StorageException { + setProperty(KEY_PASSWORD, password, runTargetId); + } + + private ISecurePreferences getSecurePreferences(String runTargetId) { + ISecurePreferences securePreferences = SecurePreferencesFactory.getDefault().node(BootDashActivator.PLUGIN_ID); + securePreferences = securePreferences.node(EncodingUtils.encodeSlashes(runTargetId)); + return securePreferences; + } + + private String readProperty(String property, String runTargetId) throws StorageException { + ISecurePreferences preferences = getSecurePreferences(runTargetId); + String val = null; + if (preferences != null) { + val = preferences.get(property, null); + //We've succesfully used it so... it must be unlocked now. + isUnlocked = true; + } + return val; + } + + private void setProperty(String property, String value, String runTargetId) throws StorageException { + ISecurePreferences preferences = getSecurePreferences(runTargetId); + if (preferences != null) { + if (value == null) { + preferences.remove(property); + } else { + preferences.put(property, value, true); + } + //We've succesfully used it so... it must be unlocked now. + isUnlocked = true; + } + } + + @Override + public boolean isUnlocked() { + return isUnlocked; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DefaultWizardModelUserInteractions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DefaultWizardModelUserInteractions.java new file mode 100644 index 000000000..8142de224 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DefaultWizardModelUserInteractions.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2016 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.model; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.progress.UIJob; +import org.springframework.ide.eclipse.boot.dash.util.UiUtil; + +public class DefaultWizardModelUserInteractions implements WizardModelUserInteractions{ + public void informationPopup(final String title, final String message) { + // Open dialogue with the error but do not prevent the creation + // of the target + UIJob job = new UIJob(title) { + + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + Shell shell = UiUtil.getShell(); + if (shell != null) { + MessageDialog.openInformation(shell, title, message); + } + return Status.OK_STATUS; + } + }; + job.schedule(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DeletionCapabableModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DeletionCapabableModel.java new file mode 100644 index 000000000..b64a0d359 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/DeletionCapabableModel.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.model; + +import java.util.Collection; + +/** + * A model that is capabable of deleting (at least some) of its elements should implement this. + * + * @author Nieraj Singh + * @author Kris De Volder + */ +public interface DeletionCapabableModel { + + void delete(Collection collection, UserInteractions ui); + + /** + * Assuming a given element belongs to this model, is this model capable of deleting the element? + */ + boolean canDelete(BootDashElement element); + + /** + * Create a message that will be shown to the user to ask them to confirm if they really want to go ahead and + * delete the given elements. + */ + String getDeletionConfirmationMessage(Collection value); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Duplicatable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Duplicatable.java new file mode 100644 index 000000000..f92f53d62 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Duplicatable.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +/** + * Interface to be implemented by BDE that support 'duplication'. + * + * @author Kris De Volder + */ +public interface Duplicatable extends BootDashElement { + + boolean canDuplicate(); + + T duplicate(UserInteractions ui); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Failable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Failable.java new file mode 100644 index 000000000..abf7bc337 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Failable.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2020 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.model; + +import org.eclipse.core.runtime.Assert; +import org.springframework.ide.eclipse.editor.support.util.HtmlSnippet; + +public class Failable { + + final private T value; + final private HtmlSnippet error; + + public static Failable of(T value) { + return new Failable<>(value, null); + } + + public static Failable error(HtmlSnippet snippet) { + return new Failable<>(null, snippet); + } + + private Failable(T value, HtmlSnippet error) { + Assert.isTrue((value != null && error == null) || (value == null && error != null)); + this.value = value; + this.error = error; + } + + @Override + public String toString() { + if (value != null) { + return value.toString(); + } else { + return error.toString(); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((error == null) ? 0 : error.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Failable other = (Failable) obj; + if (error == null) { + if (other.error != null) + return false; + } else if (!error.equals(other.error)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } + + public boolean hasFailed() { + return error != null; + } + + public HtmlSnippet getErrorMessage() { + return error; + } + + public T getValue() { + return this.value; + } + + public T orElse(T obj) { + return hasFailed() ? obj : value; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/IdAble.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/IdAble.java new file mode 100644 index 000000000..799dd930c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/IdAble.java @@ -0,0 +1,17 @@ +/******************************************************************************* + * Copyright (c) 2015 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.model; + +public interface IdAble { + + String getId(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LaunchConfDashElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LaunchConfDashElement.java new file mode 100644 index 000000000..a74da31b4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LaunchConfDashElement.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.model; + +import org.apache.commons.lang3.ArrayUtils; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.springframework.ide.eclipse.boot.dash.api.Deletable; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.util.BootLaunchUtils; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.collect.ImmutableSet; + +/** + * Concrete {@link BootDashElement} that wraps a launch config. + * + * @author Kris De Volder + */ +public class LaunchConfDashElement extends AbstractLaunchConfigurationsDashElement implements Deletable { + + private static final BootDashColumn[] COLUMNS = ArrayUtils.removeElement(LocalRunTarget.DEFAULT_COLUMNS, + BootDashColumn.DEVTOOLS + ); + + private static final boolean DEBUG = false; //(""+Platform.getLocation()).contains("kdvolder"); + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + @Override + public BootDashColumn[] getColumns() { + return COLUMNS; + } + + public LaunchConfDashElement(LocalBootDashModel bootDashModel, ILaunchConfiguration delegate) { + super(bootDashModel, delegate); + } + + @Override + protected IPropertyStore createPropertyStore() { + return PropertyStores.createFor(delegate); + } + + @Override + public ImmutableSet getLaunchConfigs() { + return ImmutableSet.of(delegate); + } + + @Override + public IProject getProject() { + return BootLaunchConfigurationDelegate.getProject(delegate); + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public ImmutableSet getLaunches() { + return ImmutableSet.copyOf(BootLaunchUtils.getLaunches(ImmutableSet.of(delegate))); + } + + @Override + public void dispose() { + super.dispose(); + debug("Disposing: "+this); + } + + @Override + public BootProjectDashElement getParent() { + IProject p = getProject(); + if (p!=null) { + return getBootDashModel().getProjectElementFactory().createOrGet(p); + } + return null; + } + + @Override + public void delete() throws CoreException { + delegate.delete(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LaunchConfDashElementFactory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LaunchConfDashElementFactory.java new file mode 100644 index 000000000..3aa37e7fd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LaunchConfDashElementFactory.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2015 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.model; + +import java.util.Map; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationListener; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchManager; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +import com.google.common.collect.MapMaker; + +/** + * @author Kris De Volder + */ +public class LaunchConfDashElementFactory implements Disposable { + + private static final boolean DEBUG = false; // (""+Platform.getLocation()).contains("kdvolder"); + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + private LocalBootDashModel model; + + private Map cache; + + private ILaunchConfigurationListener listener; + + private ILaunchManager launchManager; + + public LaunchConfDashElementFactory(LocalBootDashModel bootDashModel, ILaunchManager lm) { + this.cache = new MapMaker() + .concurrencyLevel(1) //single thread only so don't waste space for 'connurrencyLevel' support + .makeMap(); + this.model = bootDashModel; + this.launchManager = lm; + lm.addLaunchConfigurationListener(listener = new ILaunchConfigurationListener() { + + @Override + public void launchConfigurationRemoved(ILaunchConfiguration configuration) { + deleted(configuration); + } + + @Override + public void launchConfigurationChanged(ILaunchConfiguration configuration) { + } + + @Override + public void launchConfigurationAdded(ILaunchConfiguration configuration) { + } + }); + } + + private synchronized void deleted(ILaunchConfiguration configuration) { + if (this.cache!=null) { + LaunchConfDashElement element = this.cache.remove(configuration); + if (element!=null) { + debug("deleted from factory: "+element); + element.dispose(); + } + } + } + + public synchronized LaunchConfDashElement createOrGet(ILaunchConfiguration c) { + try { + if (cache!=null && c!=null) { + ILaunchConfigurationType type = c.getType(); + if (type!=null && BootLaunchConfigurationDelegate.TYPE_ID.equals(type.getIdentifier())) { + LaunchConfDashElement el = cache.get(c); + if (el==null) { + cache.put(c, el = new LaunchConfDashElement(model, c)); + debug("created: "+el); + } + return el; + } + } + } catch (Exception e) { + BootDashActivator.log(e); + } + return null; + } + + @Override + public void dispose() { + if (listener!=null) { + launchManager.removeLaunchConfigurationListener(listener); + listener = null; + launchManager = null; + } + cache = null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LocalBootDashModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LocalBootDashModel.java new file mode 100644 index 000000000..8040b2d36 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LocalBootDashModel.java @@ -0,0 +1,303 @@ +/******************************************************************************* + * 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 + * https://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.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.Platform; +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.dash.api.Deletable; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsPortRefresher; +import org.springframework.ide.eclipse.boot.dash.model.local.LocalServicesModel; +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.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; +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.LiveSets; +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.util.Log; + +/** + * 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 applications; //lazy created + private ObservableSet allElements; + + private BootDashModelConsoleManager consoleManager; + + private DevtoolsPortRefresher devtoolsPortRefresher; + private LiveExpression projectExclusion; + private ValueListener projectExclusionListener; + + private IPropertyStore modelStore; + + private LocalServicesModel localServices; + + private LiveVariable bootAppsRefreshState = new LiveVariable<>(RefreshState.READY); + + private LiveExpression refreshState; + private BundleListener bundleListener; + + 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.localServices = new LocalServicesModel(getViewModel(), this, context.getBootInstallManager().getDefaultInstallExp()); + this.refreshState = new LiveExpression() { + { + dependsOn(bootAppsRefreshState); + dependsOn(localServices.getRefreshState()); + addListener((e,v) -> notifyModelStateChanged()); + } + + @Override + protected RefreshState compute() { + return RefreshState.merge(bootAppsRefreshState.getValue(), localServices.getRefreshState().getValue()); + } + }; + 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 = PropertyStores.createForScope(type, context.getRunTargetProperties()); + this.modelStore = PropertyStores.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 = 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(bundleListener); + } + } + + void init() { + if (allElements==null) { + this.applications = new LiveSetVariable<>(AsyncMode.SYNC); + this.allElements = LiveSets.union( + this.applications, + this.localServices.getCloudCliServices() + ); + WorkspaceListener workspaceListener = new WorkspaceListener(); + this.openCloseListenerManager = new ProjectChangeListenerManager(workspace, workspaceListener); + this.classpathListenerManager = new ClasspathListenerManager(workspaceListener); + projectExclusion.addListener(projectExclusionListener = new ValueListener() { + public void gotValue(LiveExpression exp, Pattern value) { + updateElementsFromWorkspace(); + } + }); + + refresh(null); + + 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 (localServices!=null) { + localServices.dispose(); + localServices = null; + } + if (allElements != null) { + allElements.dispose(); + allElements = null; + } + launchConfTracker.dispose(); + launchConfRunStateTracker.dispose(); + // Remove bundle listener + if (bundleListener != null) { + Bundle bundle = Platform.getBundle("org.eclipse.m2e.jdt"); + if (bundle != null) { + bundle.getBundleContext().removeBundleListener(bundleListener); + } + } + + } + + void updateElementsFromWorkspace() { + LiveSetVariable apps = this.applications; + if (apps!=null) { + Set newElements = Arrays.stream(this.workspace.getRoot().getProjects()) + .map(projectElementFactory::createOrGet) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + apps.replaceAll(newElements); + projectElementFactory.disposeAllExcept(newElements); + } + } + + public synchronized ObservableSet getElements() { + init(); + return allElements; + } + + @Override + public ObservableSet getButtons() { + return localServices.getButtons(); + } + + /** + * Trigger manual model refresh. + */ + public void refresh(UserInteractions ui) { + updateElementsFromWorkspace(); + localServices.refresh(); + } + + @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 elements, UserInteractions ui) { + for (BootDashElement el : elements) { + if (el instanceof Deletable) { + try { + ((Deletable)el).delete(); + } catch (Exception e) { + Log.log(e); + } + } + } + } + + @Override + public boolean canDelete(BootDashElement element) { + return element instanceof Deletable && ((Deletable)element).canDelete(); + } + + @Override + public String getDeletionConfirmationMessage(Collection 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(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LocalCloudServiceDashElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LocalCloudServiceDashElement.java new file mode 100644 index 000000000..2bf7cadfd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LocalCloudServiceDashElement.java @@ -0,0 +1,261 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import static org.springframework.ide.eclipse.boot.dash.model.RunState.INACTIVE; +import static org.springframework.ide.eclipse.boot.dash.model.RunState.RUNNING; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +import javax.management.InstanceNotFoundException; +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.jface.resource.ImageDescriptor; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.cli.CloudCliServiceLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.util.BootLaunchUtils; +import org.springframework.ide.eclipse.boot.util.RetryUtil; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; +import org.springsource.ide.eclipse.commons.core.util.ProcessUtils; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableSet; + +/** + * Spring Cloud CLI local service boot dash element implementation + * + * @author Alex Boyko + * + */ +public class LocalCloudServiceDashElement extends AbstractLaunchConfigurationsDashElement { + + private static final EnumSet LOCAL_CLOUD_SERVICE_RUN_GOAL_STATES = EnumSet.of(INACTIVE, RUNNING); + + private static final BootDashColumn[] COLUMNS = {BootDashColumn.NAME, BootDashColumn.LIVE_PORT, BootDashColumn.RUN_STATE_ICN, BootDashColumn.TAGS}; + + private static final LoadingCache LAUNCH_CONFIG_CACHE = CacheBuilder.newBuilder().build(new CacheLoader() { + @Override + public ILaunchConfigurationWorkingCopy load(String key) throws Exception { + return CloudCliServiceLaunchConfigurationDelegate.createLaunchConfig(key); + } + }); + + public LocalCloudServiceDashElement(LocalBootDashModel bootDashModel, String id) { + super(bootDashModel, id); + } + + @Override + public IProject getProject() { + return null; + } + + public ImmutableSet getLaunches() { + List launches = new ArrayList<>(); + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunchConfigurationType type = launchManager.getLaunchConfigurationType(CloudCliServiceLaunchConfigurationDelegate.TYPE_ID); + for (ILaunch launch : launchManager.getLaunches()) { + ILaunchConfiguration configuration = launch.getLaunchConfiguration(); + try { + if (configuration!=null && configuration.getType() == type && delegate.equals(configuration.getAttribute(CloudCliServiceLaunchConfigurationDelegate.ATTR_CLOUD_SERVICE_ID, (String) null))) { + launches.add(launch); + } + } catch (CoreException e) { + Log.log(e); + } + } + return ImmutableSet.copyOf(launches); + } + + @Override + public void openConfig(UserInteractions ui) { + } + + @Override + public int getActualInstances() { + return 0; + } + + @Override + public int getDesiredInstances() { + return 0; + } + + @Override + public Object getParent() { + return getBootDashModel(); + } + + @Override + public String getName() { + return delegate; + } + + @Override + public BootDashColumn[] getColumns() { + return COLUMNS; + } + + @Override + public LocalBootDashModel getBootDashModel() { + return (LocalBootDashModel) super.getBootDashModel(); + } + + public String getId() { + return delegate; + } + + @Override + protected IPropertyStore createPropertyStore() { + return PropertyStores.createSubStore("S-"+delegate, getBootDashModel().getModelStore()); + } + + @Override + public ImmutableSet getLaunchConfigs() { + try { + return ImmutableSet.of(LAUNCH_CONFIG_CACHE.get(delegate)); + } catch (ExecutionException e) { + Log.log(e); + return ImmutableSet.of(); + } + } + + @Override + public boolean canDuplicate() { + return false; + } + + @Override + public EnumSet supportedGoalStates() { + return LOCAL_CLOUD_SERVICE_RUN_GOAL_STATES; + } + + protected LiveExpression createPortExpression(final LiveExpression runState) { + if (CloudCliServiceLaunchConfigurationDelegate.isSingleProcessServiceConfig(getActiveConfig())) { + AsyncLiveExpression exp = new AsyncLiveExpression(-1, "Refreshing port info for "+getName()) { + { + //Doesn't really depend on runState, but should be recomputed when runState changes. + dependsOn(runState); + } + @Override + protected Integer compute() { + return computeLivePort(); + } + @Override + public String toString() { + return "LivePortExp for " + getName(); + } + }; + addDisposableChild(exp); + return exp; + } + return super.createPortExpression(runState); + } + + private int computeLivePort() { + ILaunchConfiguration conf = getActiveConfig(); + if (conf != null && READY_STATES.contains(getRunState())) { + for (ILaunch l : BootLaunchUtils.getLaunches(conf)) { + if (!l.isTerminated()) { + String pid = l.getAttribute(BootLaunchConfigurationDelegate.PROCESS_ID); + if (pid != null && !pid.isEmpty()) { + JMXConnector jmxConnector = ProcessUtils.createJMXConnector(pid); + try { + MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); + // Just because lifecycle bean is ready does not mean that the port property has + // already been set. + // To avoid race condition we should wait here until the port is set (some apps + // aren't web apps and + // may never get a port set, so we shouldn't wait indefinitely!) + return RetryUtil.retry(100, 1000, () -> { + String port = getPortViaTomcatBean(connection); + if (port == null) { + throw new IllegalStateException("port not (yet) set"); + } + return Integer.valueOf(port); + }); + } catch (Exception e) { + // most likely this just means the app isn't running so ignore + } finally { + if (jmxConnector != null) { + try { + jmxConnector.close(); + } catch (IOException e) { + Log.log(e); + } + } + } + } + + } + } + } + return -1; + } + + private String getPortViaTomcatBean(MBeanServerConnection connection) throws Exception { + try { + Set queryNames = connection.queryNames(null, null); + + for (ObjectName objectName : queryNames) { + if (objectName.toString().startsWith("Tomcat") && objectName.toString().contains("type=Connector")) { + // Cloud CLI service 2.x Tomcat bean port value + Object result = connection.getAttribute(objectName, "port"); + if (result == null) { + // Older Tomcat bean format for Cloud CLI 1.x + result = connection.getAttribute(objectName, "localPort"); + } + if (result != null) { + return result.toString(); + } + } + } + } + catch (InstanceNotFoundException e) { + } + + return null; + } + + @Override + public ImageDescriptor getCustomRunStateIcon() { + switch (getRunState()) { + case RUNNING: + return BootDashActivator.getDefault().getImageRegistry().getDescriptor(BootDashActivator.SERVICE_ICON); + case STARTING: + return null; // fall back on default implementation + default: + return BootDashActivator.getDefault().getImageRegistry().getDescriptor(BootDashActivator.SERVICE_INACTIVE_ICON); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LocalRunTarget.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LocalRunTarget.java new file mode 100644 index 000000000..6abe4b925 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/LocalRunTarget.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import static org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn.*; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; + +public class LocalRunTarget extends AbstractRunTarget { + + public static final RunTarget INSTANCE = new LocalRunTarget(); + public static final BootDashColumn[] DEFAULT_COLUMNS = { + RUN_STATE_ICN, + NAME, + DEVTOOLS, + LIVE_PORT, + INSTANCES, + DEFAULT_PATH, + TAGS, + EXPOSED_URL + }; + + private LocalRunTarget() { + super(RunTargetTypes.LOCAL, "local"); + } + + /** + * Create a launch config for a given dash element and initialize it with + * some suitable defaults. + * + * @param mainType, + * may be null if the main type can not be 'guessed' + * unambiguosly. + */ + public ILaunchConfiguration createLaunchConfig(IJavaProject jp, IType mainType) throws Exception { + if (mainType != null) { + return BootLaunchConfigurationDelegate.createConf(mainType); + } else { + return BootLaunchConfigurationDelegate.createConf(jp); + } + } + + @Override + public BootDashColumn[] getDefaultColumns() { + return DEFAULT_COLUMNS; + } + + @Override + public BootDashModel createSectionModel(BootDashViewModel viewModel) { + return new LocalBootDashModel(viewModel.getContext(), viewModel); + } + + @Override + public boolean canRemove() { + return false; + } + + @Override + public boolean canDeployAppsFrom() { + return true; + } + + @Override + public Void getParams() { + return null; + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/MissingLiveInfoMessages.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/MissingLiveInfoMessages.java new file mode 100644 index 000000000..64e98a291 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/MissingLiveInfoMessages.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 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.model; + +import org.springframework.ide.eclipse.editor.support.util.HtmlSnippet; + +public interface MissingLiveInfoMessages { + + static final String EXTERNAL_DOCUMENT_LINK = "https://github.com/spring-projects/sts4/wiki/Live-Application-Information#application-requirements-for-spring-boot-projects"; + + static final MissingLiveInfoMessages DEFAULT = new MissingLiveInfoMessages() {}; + + static final HtmlSnippet NOT_YET_COMPUTED = buffer -> buffer.p("Not yet computed..."); + + default HtmlSnippet getMissingInfoMessage(String appName, String actuatorEndpoint) { + + return buffer -> { + buffer.raw("

"); + buffer.raw(""); + buffer.text(appName); + buffer.raw(""); + buffer.text(" must be running with JMX and actuator endpoint enabled:"); + buffer.raw("

"); + + buffer.raw("
    "); + + buffer.raw("
  1. "); + buffer.text("Enable actuator "); + buffer.raw(""); + buffer.text(actuatorEndpoint); + buffer.raw(""); + buffer.text(" endpoint in the application."); + buffer.raw("
  2. "); + + buffer.raw("
  3. "); + buffer.text("Select "); + buffer.raw(""); + buffer.text("Enable JMX"); + buffer.raw(""); + buffer.text(" in the application launch configuration."); + buffer.raw("
  4. "); + buffer.raw("
"); + + buffer.href(EXTERNAL_DOCUMENT_LINK, "See documentation"); + }; + + } + + static HtmlSnippet noSelectionMessage(String element) { + return buffer -> buffer.p("Select single element in Boot Dashboard to see live " + element); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/ModifiableModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/ModifiableModel.java new file mode 100644 index 000000000..c6a751f43 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/ModifiableModel.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2015 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.model; + +import java.util.List; + +public interface ModifiableModel extends DeletionCapabableModel { + boolean canBeAdded(List sources); + void add(List sources) throws Exception; +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Nameable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Nameable.java new file mode 100644 index 000000000..ae639efde --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Nameable.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2015 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.model; + +public interface Nameable { + String getName(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RefreshState.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RefreshState.java new file mode 100644 index 000000000..d4763e190 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RefreshState.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +import com.google.common.base.Objects; + +/** + * Represents the different states a 'refreshable' element may be in. + * + * @author Alex Boyko + * @author Kris De Volder + */ +public class RefreshState { + + private static enum Id { + READY, + WARNING, + ERROR, + LOADING, + } + + public static final RefreshState READY = new RefreshState(Id.READY); + public static final RefreshState LOADING = new RefreshState(Id.LOADING); + + public static RefreshState error(String msg) { + return new RefreshState(Id.ERROR, msg); + } + + public static RefreshState error(Throwable e) { + return error(ExceptionUtil.getMessage(e)); + } + + public static RefreshState warning(String message) { + return new RefreshState(Id.WARNING, message); + } + + public static RefreshState loading(String message) { + return new RefreshState(Id.LOADING, message); + } + + private Id id; + private String message; + + private RefreshState(Id id) { + this.id = id; + } + + private RefreshState(Id id, String message) { + this(id); + this.message = message; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return id + (message == null || message.isEmpty() ? "" : message); + } + + @Override + public int hashCode() { + return Objects.hashCode(id, message); + } + + @Override + public boolean equals(Object obj) { + if (obj.getClass() == getClass()) { + RefreshState other = (RefreshState) obj; + return Objects.equal(id, other.id) && Objects.equal(message, other.message); + } + return false; + } + + public boolean isError() { + return id==Id.ERROR; + } + public boolean isWarning() { + return id==Id.WARNING; + } + public boolean isLoading() { + return id==Id.LOADING; + } + + public static RefreshState merge(RefreshState s1, RefreshState s2) { + //For the caller's convenience... treat null as READY so callers don't + // need to do a bunch of null checks. + if (s1==null) { + s1 = READY; + } + if (s2==null) { + s2 = READY; + } + if (s1.id.compareTo(s2.id)<0) { + return s2; + } else { + return s1; + } + } + + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunState.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunState.java new file mode 100644 index 000000000..dfe2dcb69 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunState.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.model; + +public enum RunState { + + //Note that the order in which these are listed is important as the implementation of 'merge' + // depends on it. + UNKNOWN, + INACTIVE, + PAUSED, + STARTING, + RUNNING, + DEBUGGING, + FLAPPING, + CRASHED; // Crashed has higher priority. If at least one instance Crashed, indicate that to the user over any other state of any other instance + + /** + * Combine the runstates of two processes. This operation is used so that we can + * compute a summarized state when some entity (e.g. a workspace project) is represented + * by multiple instances that can have a RunState (e.g. launches, lattice LRPs, CF instances etc) + */ + public RunState merge(RunState other) { + if (this.ordinal()>other.ordinal()) { + return this; + } else { + return other; + } + } + + public String getImageUrl() { + String iconSuffix = "png"; + String state = toString().toLowerCase(); + if (state.contains("unknown") || state.contains("crashed") || state.contains("flapping")) { + iconSuffix = "gif"; + } + return "icons/rs_"+toString().toLowerCase()+"."+iconSuffix; + } + + public boolean isActive() { + return this == RUNNING || this == DEBUGGING; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTarget.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTarget.java new file mode 100644 index 000000000..d9c5b5993 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTarget.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * 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 + * https://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.Collection; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jface.resource.ImageDescriptor; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.util.template.Templates; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +/** + * A RunTarget represents an 'platform/environment' where we can 'Run' BootApps. + * + * TODO: launch configs are not applicable to all runtargets and methods relating + * to launch configs do not belong in here. Remove and refactor! + * + * @author Kris De Volder + */ +public interface RunTarget extends IdAble, Nameable, Disposable { + + public abstract RunTargetType getType(); + + public abstract BootDashColumn[] getDefaultColumns(); + + /** + * Factory method to create the model for the 'elements tabel' of this run + * target. + */ + public BootDashModel createSectionModel(BootDashViewModel parent); + + /** + * + * @return true if it is a run target that can be deleted (and any + * associated models). False otherwise + */ + public abstract boolean canRemove(); + + /** + * + * @return true if applications can be deployed from this run target to other run targets. False otherwise. + */ + public abstract boolean canDeployAppsFrom(); + + /** + * Provides a means to store persistent properties associated with this {@link RunTargetType} + */ + public abstract IPropertyStore getPropertyStore(); + + /** + * A convenience method that provides access to the persisent property store returned by getPropertyStore + * through more convenient API. + */ + public abstract PropertyStoreApi getPersistentProperties(); + + /** + * Return a nice name, suitable for displaying in a view to identify this target to the user. + */ + public abstract String getDisplayName(); + + /** + * Customizable template used to create the diplayName. See {@link Templates} for the syntax. + * This may return null. + */ + public abstract String getNameTemplate(); + + /** + * @return true if the String returned from getNameTemplate was specifically set on this target (instead of + * inherited from its type. + */ + public abstract boolean hasCustomNameTemplate(); + + /** + * Set a custom name template for this target. Note that this only works on targets who provides support for + * persistent properties (since that's where this value is ultimately stored). + *

+ * Setting the template to null makes the effective template be inherited from the runtarget type. + */ + public abstract void setNameTemplate(String template) throws Exception; + + /** + * Called when user double-clicks on RunTarget in the boot dash view. + */ + default void performDoubleClickAction(UserInteractions ui) {} + + default ImageDescriptor getIcon() { + return getType().getIcon(); + } + + abstract Params getParams(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargetComparator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargetComparator.java new file mode 100644 index 000000000..37c080ba3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargetComparator.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.model; + +import java.util.Comparator; +import java.util.List; + +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.util.OrderBasedComparator; + +public class RunTargetComparator implements Comparator { + + private Comparator typeComparator; + + public RunTargetComparator(Comparator typeComparator) { + this.typeComparator = typeComparator; + } + + public RunTargetComparator(List runTargetTypes) { + this(new OrderBasedComparator<>( + runTargetTypes.toArray(new RunTargetType[runTargetTypes.size()]))); + } + + public int compare(RunTarget target1, RunTarget target2) { + + RunTargetType rtType1 = target1.getType(); + RunTargetType rtType2 = target2.getType(); + + int result = typeComparator.compare(rtType1, rtType2); + if (result==0) { + result = target1.getDisplayName().compareTo(target2.getDisplayName()); + if (result==0) { + result = target1.getId().compareTo(target2.getId()); + } + } + return result; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargetPropertiesManager.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargetPropertiesManager.java new file mode 100644 index 000000000..5d023cee6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargetPropertiesManager.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springsource.ide.eclipse.commons.frameworks.core.util.ArrayEncoder; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; + +@SuppressWarnings("rawtypes") +public class RunTargetPropertiesManager implements ValueListener> { + + private final BootDashModelContext context; + + private final RunTargetType[] types; + + public static final String RUN_TARGET_KEY = "runTargets-v2"; + + public RunTargetPropertiesManager(BootDashModelContext context, Collection types) { + this(context, types.toArray(new RunTargetType[types.size()])); + } + + public RunTargetPropertiesManager(BootDashModelContext context, RunTargetType[] types) { + this.context = context; + this.types = types; + } + + @SuppressWarnings({ "unchecked" }) + public List load() { + + List targets = new ArrayList<>(); + for (RunTargetType type : types) { + if (type==RunTargetTypes.LOCAL) { + targets.add(RunTargets.LOCAL); + } else if (type.canInstantiate()) { + String serializedList = context.getRunTargetProperties().get(type, RUN_TARGET_KEY); + if (serializedList != null) { + String[] list = ArrayEncoder.decode(serializedList); + for (String serializedParams : list) { + Object params = type.parseParams(serializedParams); + try { + RunTarget target = type.createRunTarget(params); + if (target != null) { + targets.add(target); + } + } catch (Exception e) { + //log and ignore invalid run targets + Log.log(e); + } + } + } + } + } + return targets; + } + + @Override + public void gotValue(LiveExpression> exp, ImmutableSet value) { + store(value); + } + + public synchronized void store(Set _targets) { + // Only persist run target properties that can be instantiated + Multimap, RunTarget> targetsByType = MultimapBuilder.hashKeys().linkedListValues().build(); + for (RunTarget target : _targets) { + RunTargetType type = target.getType(); + if (type.canInstantiate()) { + targetsByType.put(type, target); + } + } + + for (RunTargetType type : types) { + //Careful... must iterate all types, even when there are no more targets for a given type. + // See: https://www.pivotaltracker.com/story/show/171950112 + if (type.canInstantiate()) { + try { + Collection> targets = targetsByType.get(type); + List strings = new ArrayList<>(targets.size()); + for (RunTarget t : targets) { + String s = type.serialize(t.getParams()); + if (s!=null) { + strings.add(s); + } + } + context.getRunTargetProperties().put(type, RUN_TARGET_KEY, ArrayEncoder.encode(strings.toArray(new String[strings.size()]))); + } catch (Exception e) { + Log.log(e); + } + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargetWithProperties.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargetWithProperties.java new file mode 100644 index 000000000..691070789 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargetWithProperties.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.TargetProperties; + +/** + * A run target that contains a list of properties that describe the run target. + * Not all run targets define properties (e.g. the local run target) + * + */ +public interface RunTargetWithProperties extends RunTarget { + + public TargetProperties getTargetProperties(); + + /** + * + * @return true if the target requires credentials. False otherwise + */ + public abstract boolean requiresCredentials(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargets.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargets.java new file mode 100644 index 000000000..058f688cf --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/RunTargets.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2015, 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import static org.springframework.ide.eclipse.boot.dash.model.RunState.DEBUGGING; +import static org.springframework.ide.eclipse.boot.dash.model.RunState.INACTIVE; +import static org.springframework.ide.eclipse.boot.dash.model.RunState.RUNNING; + +import java.util.EnumSet; + +public class RunTargets { + + public static final EnumSet LOCAL_RUN_GOAL_STATES = EnumSet.of(INACTIVE, RUNNING, DEBUGGING); + + public static final RunTarget LOCAL = LocalRunTarget.INSTANCE; +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/SecuredCredentialsStore.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/SecuredCredentialsStore.java new file mode 100644 index 000000000..5ee506271 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/SecuredCredentialsStore.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import org.eclipse.equinox.security.storage.StorageException; + +public interface SecuredCredentialsStore { + + String getCredentials(String string) throws StorageException; + void setCredentials(String runTargetId, String password) throws StorageException; + boolean isUnlocked(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/TagSearchFilter.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/TagSearchFilter.java new file mode 100644 index 000000000..e1f4cba7c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/TagSearchFilter.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import org.springsource.ide.eclipse.commons.core.PatternUtils; +import org.springsource.ide.eclipse.commons.livexp.util.Filter; + +import com.google.common.collect.ImmutableSet; + +/** + * The filter for searching for tags. + * + * @author Alex Boyko + * @author Kris De Volder + */ +public class TagSearchFilter implements Filter { + + private String searchTerm; + + private String[] searchTags; + + public TagSearchFilter() { + this(null, null); + } + + public TagSearchFilter(String[] searchTags, String searchTerm) { + this.searchTags = searchTags == null ? new String[0] : searchTags; + this.searchTerm = searchTerm == null ? "" : searchTerm; + } + + public TagSearchFilter(String s) { + this(); + if (!s.isEmpty()) { + String[] splitSearchStr = TagUtils.parseTags(s); + if (splitSearchStr.length > 0) { + if (Pattern.matches("(.+)" + TagUtils.SEPARATOR_REGEX, s)) { + this.searchTags = splitSearchStr; + } else { + this.searchTags = Arrays.copyOfRange(splitSearchStr, 0, splitSearchStr.length - 1); + this.searchTerm = splitSearchStr[splitSearchStr.length - 1]; + } + } + } + } + + @Override + public boolean accept(T element) { + if (searchTags.length == 0 && searchTerm.isEmpty()) { + return true; + } + + List> patterns = new ArrayList<>(searchTags.length); + for (String searchTag : searchTags) { + patterns.add(toPattern(searchTag)); + } + patterns.add(toPattern("*"+searchTerm+"*")); + + Set elementTags = getTags(element); + + return patterns.stream().allMatch( + (pat) -> elementTags.stream().anyMatch(pat) + ); + } + + private Predicate toPattern(String wildcarded) { + Pattern pat = PatternUtils.createPattern(wildcarded, false, false); + return (s) -> pat.matcher(s).matches(); + } + + @Override + public String toString() { + String initSearchText = TagUtils.toString(searchTags); + if (!searchTerm.isEmpty()) { + initSearchText += TagUtils.SEPARATOR + searchTerm; + } + return initSearchText; + } + + protected ImmutableSet getTags(T element) { + LinkedHashSet tags = element.getTags(); + if (tags==null) { + return ImmutableSet.of(); + } + return ImmutableSet.copyOf(tags); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/TagUtils.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/TagUtils.java new file mode 100644 index 000000000..568ac605f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/TagUtils.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.jface.viewers.StyledString.Styler; + +/** + * Utilities for generating tags array from text, generating string from tags array and others + * + * @author Alex Boyko + * + */ +public class TagUtils { + + public static final char SEPARATOR_SYMBOL = ','; + + /** + * String separator between tags string representation + */ + public static final String SEPARATOR = SEPARATOR_SYMBOL + " "; + + /** + * Regular Expression pattern for separation string between tags in their textual representation + */ + public static final String SEPARATOR_REGEX = "\\s*" + SEPARATOR_SYMBOL + "\\s*"; + + /** + * Parses text into tags + * + * @param text the string text + * @return array of string tags + */ + public static String[] parseTags(String text) { + String s = text.trim(); + if (s.isEmpty()) { + return new String[0]; + } else { + String[] split = s.split(SEPARATOR_REGEX); + if (split.length > 0) { + ArrayList sanitized = new ArrayList<>(split.length); + for (String tag : split) { + if (!tag.isEmpty()) { + sanitized.add(tag); + } + } + split = sanitized.toArray(new String[sanitized.size()]); + } + return split; + } + + } + + /** + * Generates string representation for tags + * + * @param tags the tags + * @return the string representation of the tags + */ + public static String toString(Collection tags) { + return StringUtils.join(tags, SEPARATOR); + } + + /** + * Generates string representation for tags + * + * @param tags the tags + * @return the string representation of the tags + */ + public static String toString(String[] tags) { + return StringUtils.join(tags, SEPARATOR); + } + + /** + * Creates styled string applying tagStyle at appropriate locations in a raw tags string. + */ + public static StyledString applyTagStyles(String text, Styler tagStyler) { + StyledString styledString = new StyledString(text); + Matcher matcher = Pattern.compile(SEPARATOR_REGEX).matcher(text); + int position = 0; + while (matcher.find()) { + if (position < matcher.start()) { + styledString.setStyle(position, matcher.start() - position, tagStyler); + } + position = matcher.end(); + } + if (position < text.length()) { + styledString.setStyle(position, text.length() - position, tagStyler); + } + return styledString; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Taggable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Taggable.java new file mode 100644 index 000000000..86deadf39 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/Taggable.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2015 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.model; + +import java.util.LinkedHashSet; + +/** + * Interface for taggable items + * + * @author Alex Boyko + * + */ +public interface Taggable { + + /** + * Returns an ordered set of string tags + * @return array of tags + */ + LinkedHashSet getTags(); + + + /** + * Sets an ordered set of new tags + * @param newTags new tags + */ + void setTags(LinkedHashSet newTags); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/ToggleFiltersModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/ToggleFiltersModel.java new file mode 100644 index 000000000..3c856e129 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/ToggleFiltersModel.java @@ -0,0 +1,190 @@ +/******************************************************************************* + * 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 + * https://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.HashSet; +import java.util.Set; + +import org.eclipse.core.resources.IProject; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; +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.ValueListener; +import org.springsource.ide.eclipse.commons.livexp.ui.Ilabelable; +import org.springsource.ide.eclipse.commons.livexp.util.Filter; +import org.springsource.ide.eclipse.commons.livexp.util.Filters; + +import com.google.common.collect.ImmutableSet; + +/** + * The 'toggle filters' are fixed set of view filters that can be toggled on/off by the user. + * This model element tracks the currently selected 'toggle' filters and the corresponding composite + * filter that results from composing them. + * + * @author Kris De Volder + */ +public class ToggleFiltersModel { + + private static final Filter HIDE_SOLITARY_CONFS = new Filter() { + public boolean accept(BootDashElement e) { + if (e instanceof LaunchConfDashElement) { + LaunchConfDashElement conf = (LaunchConfDashElement) e; + return conf.getParent().getCurrentChildren().size()!=1; + } + return true; + } + }; + + private static final Filter HIDE_NON_WORKSPACE_ELEMENTS = new Filter() { + public boolean accept(BootDashElement t) { + if (t!=null) { + IProject p = t.getProject(); + return p!=null && p.exists(); + } + return false; + } + }; + + private static final Filter HIDE_LOCAL_SERVICES = new Filter() { + @Override + public boolean accept(BootDashElement t) { + return !(t instanceof LocalCloudServiceDashElement); + } + }; + + public static final FilterChoice FILTER_CHOICE_HIDE_NON_WORKSPACE_ELEMENTS = new FilterChoice("hide.non-workspace", + "Hide non-workspace elements", HIDE_NON_WORKSPACE_ELEMENTS); + public static final FilterChoice FILTER_CHOICE_HIDE_SOLITARY_CONFS = new FilterChoice("hide.solitary-launch-config", + "Hide solitary launch configs", HIDE_SOLITARY_CONFS, true); + public static final FilterChoice FILTER_CHOICE_HIDE_LOCAL_SERVICES = new FilterChoice("hide.local-cloud-services", + "Hide local cloud services", HIDE_LOCAL_SERVICES, true); + + private static final String STORE_ID = "toggle-filters"; + private static final FilterChoice[] FILTERS = { + FILTER_CHOICE_HIDE_NON_WORKSPACE_ELEMENTS, + FILTER_CHOICE_HIDE_SOLITARY_CONFS, + FILTER_CHOICE_HIDE_LOCAL_SERVICES + }; + + private final PropertyStoreApi persistentProperties; + + public ToggleFiltersModel(BootDashModelContext context) { + this(PropertyStores.createSubStore(STORE_ID, context.getViewProperties())); + } + + public ToggleFiltersModel(IPropertyStore propertyStore) { + this.persistentProperties = new PropertyStoreApi(propertyStore); + this.selectedFilters = new LiveSetVariable<>(restoreFilters(), AsyncMode.SYNC); + this.compositeFilter = new LiveExpression>() { + { + dependsOn(selectedFilters); + } + @Override + protected Filter compute() { + Filter composed = Filters.acceptAll(); + for (FilterChoice chosen : selectedFilters.getValues()) { + composed = Filters.compose(composed, chosen.getFilter()); + } + return composed; + } + }; + + selectedFilters.addListener(new ValueListener>() { + public void gotValue(LiveExpression> exp, ImmutableSet value) { + saveFilters(value); + } + }); + } + + public static class FilterChoice implements Ilabelable { + private final String id; + private final String label; + private final Filter filter; + private final boolean defaultEnable; + + public FilterChoice(String id, String label, Filter filter) { + this(id, label, filter, false); + } + + public FilterChoice(String id, String label, Filter filter, boolean defaultEnable) { + this.id = id; + this.label = label; + this.filter = filter; + this.defaultEnable = defaultEnable; + } + + @Override + public String toString() { + return "FilterChoice("+getLabel()+")"; + } + + @Override + public String getLabel() { + return label; + } + + public Filter getFilter() { + return filter; + } + + public String getId() { + return id; + } + } + + private final LiveSetVariable selectedFilters; + private final LiveExpression> compositeFilter; + + /** + * @return The filter that is defined by composing all the selected toggle filters. + */ + public LiveExpression> getFilter() { + return compositeFilter; + } + private Set restoreFilters() { + Set builder = new HashSet<>(); + for (FilterChoice filter : getAvailableFilters()) { + if (persistentProperties.get(filter.getId(), filter.defaultEnable)) { + builder.add(filter); + } + } + return builder; + } + + private void saveFilters(ImmutableSet filters) { + try { + for (FilterChoice f : getAvailableFilters()) { + boolean active = filters.contains(f); + if (active==f.defaultEnable) { + //don't store default values that way if we change the default in the future then + // users will get the new default rather than their persisted value + persistentProperties.put(f.getId(), (String)null); + } else { + persistentProperties.put(f.getId(), active); + } + } + } catch (Exception e) { + //trouble saving filters... log and move on. This is not critical + Log.log(e); + } + } + + public FilterChoice[] getAvailableFilters() { + return FILTERS; + } + public LiveSetVariable getSelectedFilters() { + return selectedFilters; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/UserInteractions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/UserInteractions.java new file mode 100644 index 000000000..8f6fdf785 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/UserInteractions.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model; + +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.core.IType; +import org.eclipse.jface.dialogs.IInputValidator; +import org.springframework.ide.eclipse.boot.dash.dialogs.EditTemplateDialog; +import org.springframework.ide.eclipse.boot.dash.dialogs.EditTemplateDialogModel; +import org.springframework.ide.eclipse.boot.dash.dialogs.ToggleFiltersDialogModel; + +/** + * An instance of this interface handles interactions with the GUI code from + * the model code. It's main purpose is to provide a convenient handle for + * 'mocking' GUI interactions in test code, using, for example, mockito. + * + * @author Kris De Volder + */ +public interface UserInteractions { + ILaunchConfiguration chooseConfigurationDialog(String dialogTitle, String message, Collection configs); + IType chooseMainType(IType[] mainTypes, String dialogTitle, String message); + void errorPopup(String title, String message); + void warningPopup(String title, String message); + void openLaunchConfigurationDialogOnGroup(ILaunchConfiguration selection, String launchGroup); + void openUrl(String url); + boolean confirmOperation(String title, String message); + void openDialog(ToggleFiltersDialogModel model); + String selectRemoteEureka(BootDashViewModel model, String title, String message, String initialValue, IInputValidator validator); + + int confirmOperation(String title, String message, String[] buttonLabels, int defaultButtonIndex); + + /** + * Ask the user to select a file. + * @param title The title of the open file dialog + * @param file The default path/file that should be used when opening the dialog + * @return The full path of the selected file + */ + String chooseFile(String title, String file); + + /** + * Ask the user to confirm or cancel an operation, with a toggle option. + * + * @param propertyKey a preference name that will be used to remember the state of the 'toggle' option. + * @param title Title for the dialog + * @param message Detailed message + * @param toggleMessage Message for the 'togle switch'. + */ + boolean confirmWithToggle(String propertyKey, String title, String message, String toggleMessage); + + + /** + * Ask the user to answer 'yes' or 'no' to a question with a 'toggle' to optionally remember the answer. + * + * @param propertyKey a preference name that will be used to remember the state of the 'toggle' and 'answer'. + * @param title Title for the dialog + * @param message Detailed message + * @param toggleMessage Message for the 'togle switch'. + */ + boolean yesNoWithToggle(String propertyKey, String title, String message, String toggleMessage); + + /** + * Opens a {@link EditTemplateDialog} on given dialog model. + */ + void openEditTemplateDialog(EditTemplateDialogModel model); + + /** + * Prompt the user to choose an element from a list. + * + * @return The chosen element or null (if user canceled the dialog). + */ + T chooseElement(String title, String message, List elemments, Function labelProvider); + + /** + * Prompt user to enter a simple string value. + */ + String inputDialog(String dialogTitle, String prompt, String defaultValue); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/WizardModelUserInteractions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/WizardModelUserInteractions.java new file mode 100644 index 000000000..85b7511ca --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/WizardModelUserInteractions.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2016 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.model; + +/** + * TODO: Should be merged with {@link UserInteractions} + * This is just a workaround to avoid heavy refactoring to pass in the "main" user interactions + * to wizard models + */ +public interface WizardModelUserInteractions { + + void informationPopup(String title, String message); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/WrappingBootDashElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/WrappingBootDashElement.java new file mode 100644 index 000000000..eeea6ad82 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/WrappingBootDashElement.java @@ -0,0 +1,294 @@ +/******************************************************************************* + * 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 + * https://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.LinkedHashSet; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookupImpl; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens; +import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; +import org.springframework.ide.eclipse.boot.dash.util.Utils; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +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.livexp.core.AbstractDisposable; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSets; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +import com.google.common.collect.ImmutableSet; + +/** + * Abstract base class that is convenient to implement {@link BootDashElement}. + * @author Kris De Volder + */ +public abstract class WrappingBootDashElement extends AbstractDisposable implements BootDashElement { + + public static final String TAGS_KEY = "tags"; + + private static final String DEFAULT_RM_PATH_KEY = "default.request-mapping.path"; + public static final String DEFAULT_RM_PATH_DEFAULT = "/"; + + protected final T delegate; + + private CancelationTokens cancelationTokens = new CancelationTokens(); + + private BootDashModel bootDashModel; + private TypeLookup typeLookup; + + @SuppressWarnings("rawtypes") + private ValueListener elementStateNotifier = new ValueListener() { + public void gotValue(LiveExpression exp, Object value) { + getBootDashModel().notifyElementChanged(WrappingBootDashElement.this, "ValueChanged("+exp+", "+value+")"); + } + }; + + private ValueListener elementNotifier; + + @Override + public BootDashColumn[] getColumns() { + return getTarget().getDefaultColumns(); + } + + public WrappingBootDashElement(BootDashModel bootDashModel, T delegate) { + this.bootDashModel = bootDashModel; + this.delegate = delegate; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((delegate == null) ? 0 : delegate.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + @SuppressWarnings("rawtypes") + WrappingBootDashElement other = (WrappingBootDashElement) obj; + if (delegate == null) { + if (other.delegate != null) + return false; + } else if (!delegate.equals(other.delegate)) + return false; + return true; + } + + + @Override + public String toString() { + return delegate.toString(); + } + + protected TypeLookup getTypeLookup() { + if (typeLookup==null) { + typeLookup = new TypeLookupImpl(getName(), getProject()); + } + return typeLookup; + } + + public abstract PropertyStoreApi getPersistentProperties(); + + @Override + public LinkedHashSet getTags() { + try { + String[] tags = getPersistentProperties() == null ? null : getPersistentProperties().get(TAGS_KEY, (String[])null); + if (tags!=null) { + return new LinkedHashSet<>(Arrays.asList(tags)); + } + } catch (Exception e) { + BootDashActivator.log(e); + } + return new LinkedHashSet<>(); + } + + @Override + public void setTags(LinkedHashSet newTags) { + try { + if (newTags==null || newTags.isEmpty()) { + getPersistentProperties().put(TAGS_KEY, (String[])null); + } else { + getPersistentProperties().put(TAGS_KEY, newTags.toArray(new String[newTags.size()])); + } + bootDashModel.notifyElementChanged(this, "setTags"); + } catch (Exception e) { + BootDashActivator.log(e); + } + } + + @Override + public final String getDefaultRequestMappingPath() { + String storedValue = getPersistentProperties() == null ? null : getPersistentProperties().get(DEFAULT_RM_PATH_KEY); + if (storedValue!=null) { + return storedValue; + } + //inherit a default value from parent node? + Object parent = getParent(); + if (parent instanceof BootDashElement) { + String inheritedValue = ((BootDashElement) parent).getDefaultRequestMappingPath(); + return inheritedValue; + } + return null; + } + + @Override + public final void setDefaultRequestMappingPath(String defaultPath) { + try { + getPersistentProperties().put(DEFAULT_RM_PATH_KEY, defaultPath); + getBootDashModel().notifyElementChanged(this, "setDefaultRequestMappingPath"); + } catch (Exception e) { + BootDashActivator.log(e); + } + } + + private LiveExpression hasDevtools = null; + + @Override + public final boolean projectHasDevtoolsDependency() { + if (hasDevtools==null) { + hasDevtools = new LiveExpression(false) { + @Override + protected Boolean compute() { + boolean val = BootPropertyTester.hasDevtools(getProject()); + return val; + } + }; + hasDevtools.refresh(); + ClasspathListenerManager classpathListener = new ClasspathListenerManager(new ClasspathListener() { + public void classpathChanged(IJavaProject jp) { + if (jp.getProject().equals(getProject())) { + hasDevtools.refresh(); + } + } + }); + this.dependsOn(hasDevtools); + this.addDisposableChild(classpathListener); + this.addDisposableChild(hasDevtools); + } + return hasDevtools.getValue(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected void dependsOn(LiveExpression liveProperty) { + liveProperty.addListener(new ValueListener() { + public void gotValue(LiveExpression exp, Object value) { + getBootDashModel().notifyElementChanged(WrappingBootDashElement.this, "livePropertyChanged("+exp+", "+value+")"); + } + }); + } + + public BootDashModel getBootDashModel() { + return bootDashModel; + } + + @Override + public IJavaProject getJavaProject() { + return getProject() != null ? JavaCore.create(getProject()) : null; + } + + @Override + public ObservableSet getChildren() { + return LiveSets.emptySet(BootDashElement.class); + } + + @Override + public ImmutableSet getCurrentChildren() { + return getChildren().getValue(); + } + + @Override + public ImmutableSet getLaunchConfigs() { + //Default implementation for BDEs that do not have any relation to launch configs + //Subclass should override when elements relate to launch configs. + return ImmutableSet.of(); + } + + /** + * Gets a summary of the 'livePorts' for this node and its children. This default implementation + * is provided for nodes that only have a single port. Nodes that need to compute an actual + * summary should override this. + */ + @Override + public ImmutableSet getLivePorts() { + int port = getLivePort(); + if (port>0) { + return ImmutableSet.of(port); + } else { + return ImmutableSet.of(); + } + } + + /** + * Ensure that element state notifications are fired when a given liveExp's value changes. + */ + @SuppressWarnings("unchecked") + protected void addElementState(LiveExpression state) { + state.addListener(elementStateNotifier); + } + + @Override + public String getUrl() { + return Utils.createUrl(getLiveHost(), getLivePort(), getDefaultRequestMappingPath()); + } + + private synchronized ValueListener getElementNotifier() { + if (elementNotifier==null) { + elementNotifier = new ValueListener() { + @Override + public void gotValue(LiveExpression exp, Object value) { + getBootDashModel().notifyElementChanged(WrappingBootDashElement.this, "ValueChanged("+exp+", "+value+")"); + } + }; + } + return elementNotifier; + } + + /** + * Attach a listener to a given liveExp so that the model's 'notifyElementChanged' is called + * any time the liveExps value changes. + */ + @SuppressWarnings("unchecked") + protected void addElementNotifier(LiveExpression exp) { + @SuppressWarnings("rawtypes") + ValueListener notifier = getElementNotifier(); + exp.addListener(notifier); + } + + public CancelationToken createCancelationToken() { + return cancelationTokens.create(); + } + + public void cancelOperations() { + cancelationTokens.cancelAll(); + } + + @Override + public RunTarget getTarget() { + return getBootDashModel().getRunTarget(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/AbstractRequestMapping.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/AbstractRequestMapping.java new file mode 100644 index 000000000..5d5c934c2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/AbstractRequestMapping.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2018 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.model.actuator; + +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.IType; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * Base implementation of the request mapping interface with type look up + * + * @author Alex Boyko + * + */ +public abstract class AbstractRequestMapping implements RequestMapping { + + protected final TypeLookup typeLookup; + + public AbstractRequestMapping(TypeLookup typeLookup) { + this.typeLookup = typeLookup; + } + + @Override + public IType getType() { + String fqName = getFullyQualifiedClassName(); + if (fqName!=null) { + return typeLookup.findType(fqName); + } + return null; + } + + @Override + public boolean isUserDefined() { + try { + IType type = getType(); + if (type!=null) { + IPackageFragmentRoot pfr = (IPackageFragmentRoot)type.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT); + if (pfr!=null) { + return pfr.getKind()==IPackageFragmentRoot.K_SOURCE; + } + } + } catch (Exception e) { + Log.log(e); + } + return false; + } + + @Override + public IMethod getMethod() { + try { + IType type = getType(); + if (type!=null) { + String mName = getMethodName(); + if (mName!=null) { + IMethod[] methods = type.getMethods(); + if (methods!=null && methods.length>0) { + for (IMethod m : methods) { + //TODO: handle method overloading + if (mName.equals(m.getElementName())) { + return m; + } + } + } + } + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/ActuatorClient.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/ActuatorClient.java new file mode 100644 index 000000000..ad057c01f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/ActuatorClient.java @@ -0,0 +1,138 @@ +/******************************************************************************* + * 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.model.actuator; + +import java.util.List; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.json.JSONException; +import org.json.JSONObject; +import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; +import org.springframework.ide.eclipse.beans.ui.live.model.JsonParser; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansJsonParser; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansJsonParser2; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansModel; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.LiveEnvJsonParser1x; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.LiveEnvJsonParser2x; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.LiveEnvModel; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * Abstract implementation of a ActuatorClient. The actuar client connects + * to an actuator endpoint retrieving some information from a running spring boot app. + *

+ * This implementation is abstract because there is more than one way that we can + * connect to an actuator endpoint and retrieve the data from it. The method + * to retrieve the data is therefore an abstract method. + * + * @author Kris De Volder + */ +public abstract class ActuatorClient { + + private static final VersionRange BEANS_PARSER_VERSION_1_RANGE = new VersionRange("[1.0.0, 2.0.0)"); + //Note: this use of osgi.framework.VersionRange appears to be okay for now. + // It is not applied to a version derived from spring boot itself but to a version from + // 'org.springframework.ide.eclipse.boot.dash.model.actuator.JMXActuatorClient.OperationInfo' + // This is data fully produced by us amd just consisting of a simple number like "1" or "2" at the moment, + // OSGI version parser handles it fine. (Whereas spring-boot specific version parser actually does not). + + private final TypeLookup typeLookup; + + public ActuatorClient(TypeLookup typeLookup) { + this.typeLookup = typeLookup; + } + + + private List parseRequestMappings(String json, String version) throws JSONException { + JSONObject obj = new JSONObject(json); + RequestMappingsParser parser; + if ("2".equals(version)) { + // Boot 2.x + parser = new Boot2RequestMappingsParser(); + } else { + //Boot 1.x + parser = new Boot1RequestMappingsParser(); + } + return parser.parse(obj, typeLookup); + } + + public List getRequestMappings() { + try { + ImmutablePair data = getRequestMappingData(); + if (data != null) { + String json = data.left; + if (json!=null) { + return parseRequestMappings(json, data.right); + } + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + public LiveBeansModel getBeans() { + try { + ImmutablePair data = getBeansData(); + if (data != null) { + String json = data.left; + String version = data.right; + if (json != null) { + if (version != null) { + if (BEANS_PARSER_VERSION_1_RANGE.includes(Version.valueOf(version))) { + return new LiveBeansJsonParser(typeLookup, json).parse(); + } + } + return new LiveBeansJsonParser2(typeLookup, json).parse(); + } + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + public LiveEnvModel getEnv() { + try { + ImmutablePair data = getEnvData(); + if (data != null) { + String json = data.left; + if (json!=null) { + return parseEnv(json, data.right); + } + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + private LiveEnvModel parseEnv(String json, String version) throws Exception { + JsonParser parser = null; + if ("2".equals(version)) { + // Boot 2.x + parser = new LiveEnvJsonParser2x(); + } else { + //Boot 1.x + parser = new LiveEnvJsonParser1x(); + } + return parser.parse(json); + } + + protected abstract ImmutablePair getRequestMappingData() throws Exception; + + protected abstract ImmutablePair getBeansData() throws Exception; + + protected abstract ImmutablePair getEnvData() throws Exception; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/Boot1RequestMappingsParser.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/Boot1RequestMappingsParser.java new file mode 100644 index 000000000..7f2aafcd8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/Boot1RequestMappingsParser.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2018 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.model.actuator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; + +public class Boot1RequestMappingsParser implements RequestMappingsParser { + + /* + There are two styles of entries: + + 1) key is a 'path' String. May contain patters like "**" + "/** /favicon.ico":{ + "bean":"faviconHandlerMapping" + } + + 2) key is a 'almost json' String + "{[/bye],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}":{ + "bean":"requestMappingHandlerMapping", + "method":"public java.lang.String demo.MyController.bye()" + } + */ + + + + @Override + public List parse(JSONObject obj, TypeLookup typeLookup) throws JSONException { + @SuppressWarnings("unchecked") + Iterator keys = obj.keys(); + List result = new ArrayList<>(); + while (keys.hasNext()) { + String rawKey = keys.next(); + JSONObject value = obj.getJSONObject(rawKey); + Collection mappings = RequestMapping1x.create(rawKey, value.optString("method"), typeLookup); + result.addAll(mappings); + } + return result; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/Boot2RequestMappingsParser.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/Boot2RequestMappingsParser.java new file mode 100644 index 000000000..62cb9f725 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/Boot2RequestMappingsParser.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2018, 2020 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.model.actuator; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; + +public class Boot2RequestMappingsParser implements RequestMappingsParser { + + @Override + public List parse(JSONObject obj, TypeLookup typeLookup) throws JSONException { + JSONObject contexts = obj.getJSONObject("contexts"); + List result = new ArrayList<>(); + for (String contextId : keys(contexts)) { + JSONObject mappings = contexts.getJSONObject(contextId).getJSONObject("mappings"); + + JSONArray rmArray = null; + if (mappings.has("dispatcherServlets")) { + // Regular Web starter endpoints RMs JMX beans format + rmArray = mappings.getJSONObject("dispatcherServlets").optJSONArray("dispatcherServlet"); + } else if (mappings.has("dispatcherHandlers")) { + // WebFlux endpoints RMs JMX bean format + rmArray = mappings.getJSONObject("dispatcherHandlers").optJSONArray("webHandler"); + } + + if (rmArray != null) { + for (int i = 0; i < rmArray.length(); i++) { + JSONObject servlet = rmArray.getJSONObject(i); + JSONObject details = servlet.optJSONObject("details"); + if (details == null) { + // Fall back to 1.x for missing "details" property, i.e. no method handler defined + result.addAll(RequestMapping1x.create(servlet.getString("predicate"), servlet.getString("handler"), typeLookup)); + } else { + if (details.optJSONObject("handlerFunction") != null) { + result.addAll(RequestMapping2x.createWebFlux(typeLookup, servlet.optString("predicate"), details)); + } else { + result.addAll(RequestMapping2x.create(typeLookup, servlet.getString("handler"), details)); + } + } + } + } + + if (mappings.has("servlets")) { + JSONArray servlets = mappings.getJSONArray("servlets"); + for (int i = 0; i < servlets.length(); i++) { + JSONObject servlet = servlets.getJSONObject(i); + result.addAll(RequestMapping2x.createFromSimpleServlet(typeLookup, servlet)); + } + } + + } + return result; + } + + /** + * Convenience method, makes up for the fact that in Eclipse land we have to use an ancient version of org.json. + */ + private Iterable keys(JSONObject obj) { + return obj::keys; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/JLRMethodParser.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/JLRMethodParser.java new file mode 100644 index 000000000..f3bc1db27 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/JLRMethodParser.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model.actuator; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Uiltily methods for extracting info out of the 'toString' values produced by java.lang.reflect.Method + * objects. + * + * @author Kris De Volder + */ +public class JLRMethodParser { + + public static class JLRMethod { + + /** + * The whole 'raw' string (i.e. not parsed or processed in any way). + */ + private String rawString; + + private String fqClass; + private String methodName; + + //TODO: parsing arguments to handle overloading + + public JLRMethod(String method) { + this.rawString = method; + String methodString = method; + // Example: public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(java.security.Principal) + // Example: java.util.Collection demo.ReservationRestController.reservations() + // public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)' + + //The spaces inside generics will mess this klunky parser up. So get rid of those first: + methodString = methodString.replaceAll(",\\s", ","); + String[] pieces = methodString.split("\\s"); + int modifiersEnd = 0; + while (modifiersEnd=modifiersEnd+2) { + methodString = pieces[modifiersEnd+1]; + int methodNameEnd = methodString.indexOf('('); + if (methodNameEnd>=0) { + int methodNameStart = methodString.lastIndexOf('.', methodNameEnd); + if (methodNameStart>=0) { + fqClass = methodString.substring(0, methodNameStart); + if (methodNameStart>=0) { + methodNameStart = methodNameStart +1; //+1 because actauly pointing at the '.', not the name start + } + methodName = methodString.substring(methodNameStart, methodNameEnd); + } + } + } + } + + @Override + public String toString() { + return rawString; + } + + public String getFQClassName() { + return fqClass; + } + + public String getMethodName() { + return methodName; + } + + } + + private static final Set MODIFIERS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "public", "protected", "private", "abstract", + "static", "final", "synchronized", "native", "strictfp" + ))); + + private static boolean isModifier(String string) { + return MODIFIERS.contains(string); + } + + public static JLRMethod parse(String method) { + if (method!=null) { + return new JLRMethod(method); + } + return null; + } + + public static String parseFQClassName(String data) { + JLRMethod m = parse(data); + if (m!=null) { + return m.getFQClassName(); + } + return null; + } + + public static String parseMethodName(String data) { + JLRMethod m = parse(data); + if (m!=null) { + return m.getMethodName(); + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/JMXActuatorClient.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/JMXActuatorClient.java new file mode 100644 index 000000000..0ad8daf92 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/JMXActuatorClient.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) 2017, 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.model.actuator; + +import java.util.Set; +import java.util.function.Supplier; + +import javax.management.InstanceNotFoundException; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; +import org.springframework.ide.eclipse.boot.launch.util.JMXClient; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableSet; + +/** + * Concretization of abstract {@link ActuatorClient} which uses JMX to connect + * to actuator endpoint(s). + * + * @author Kris De Volder + */ +public class JMXActuatorClient extends ActuatorClient { + + private final Supplier urlProvider; + + private String url; + + static class OperationInfo { + final String objectName; + final String operationName; + final String version; + public OperationInfo(String objectName, String operationName, String version) { + this.objectName = objectName; + this.operationName = operationName; + this.version = version; + } + } + + private static final OperationInfo[] REQUEST_MAPPINGS_OPERATIONS = { + new OperationInfo("org.springframework.boot:type=Endpoint,name=Mappings", "mappings", "2"), //Boot 2.x + new OperationInfo("org.springframework.boot:type=Endpoint,name=requestMappingEndpoint", "getData", "1") //Boot 1.x + }; + + private static final OperationInfo[] BEANS_OPERATIONS = { + new OperationInfo("org.springframework.boot:type=Endpoint,name=Beans", "beans", "2"), //Boot 2.x + new OperationInfo("org.springframework.boot:type=Endpoint,name=beansEndpoint", "getData", "1") //Boot 1.x + }; + + private static final OperationInfo[] ENV_OPERATIONS = { + new OperationInfo("org.springframework.boot:type=Endpoint,name=Env", "environment", "2"), //Boot 2.x + new OperationInfo("org.springframework.boot:type=Endpoint,name=environmentEndpoint", "getData", "1") //Boot 1.x + }; + + private JMXClient client = null; + + public static JMXActuatorClient forPort(TypeLookup typeLookup, Supplier jmxPort) { + return new JMXActuatorClient(typeLookup, () -> JMXClient.createLocalJmxUrl(jmxPort.get())); + } + + public static JMXActuatorClient forUrl(TypeLookup typeLookup, Supplier jmxUrl) { + return new JMXActuatorClient(typeLookup, jmxUrl); + } + + private JMXActuatorClient(TypeLookup typeLookup, Supplier jmxUrl) { + super(typeLookup); + this.urlProvider = jmxUrl; + } + + @Override + protected ImmutablePair getRequestMappingData() throws Exception { + return getDataFrom(REQUEST_MAPPINGS_OPERATIONS); + } + + @Override + protected ImmutablePair getBeansData() throws Exception { + return getDataFrom(BEANS_OPERATIONS); + } + + @Override + protected ImmutablePair getEnvData() throws Exception { + return getDataFrom(ENV_OPERATIONS); + } + + protected ImmutablePair getDataFrom(OperationInfo[] infos) throws Exception { + try { + JMXClient client = getClient(); + if (client!=null && infos!=null) { + for (OperationInfo op : infos) { + try { + Object obj = client.callOperation(op.objectName, op.operationName); + if (obj!=null) { + return ImmutablePair.of(new ObjectMapper().writeValueAsString(obj), op.version); + } + } catch (InstanceNotFoundException e) { + //Ignore and try other mbean + } + } + } + } catch (Exception e) { + disposeClient(); //Client may be in broken state, do not reuse. + if (!isExpectedException(e)) { + throw e; + } + } + return null; + } + + private static final Set EXPECTED_EXCEPTIONS = ImmutableSet.of( + "ConnectException", + "InstanceNotFoundException" + ); + + private boolean isExpectedException(Exception _e) { + Throwable e = ExceptionUtil.getDeepestCause(_e); + String className = e.getClass().getSimpleName(); + return EXPECTED_EXCEPTIONS.contains(className); + } + + private synchronized JMXClient getClient() throws Exception { + String currentUrl = urlProvider.get(); + if (currentUrl==null) return null; + if (!currentUrl.equals(this.url) || client==null) { + disposeClient(); + url = currentUrl; + client = new JMXClient(currentUrl); + } + return client; + } + + private void disposeClient() { + JMXClient client = this.client; + if (client!=null) { + this.client = null; + client.dispose(); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMapping.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMapping.java new file mode 100644 index 000000000..a80588b3e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMapping.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model.actuator; + +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IType; + +public interface RequestMapping { + public String getPath(); + public String getFullyQualifiedClassName(); + public String getMethodName(); + public IType getType(); + public IMethod getMethod(); + public boolean isUserDefined(); + public String getMethodString(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMapping1x.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMapping1x.java new file mode 100644 index 000000000..2efef06a0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMapping1x.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright (c) 2015, 2018 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.model.actuator; + +import java.util.Collection; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; +import org.springframework.ide.eclipse.boot.dash.model.actuator.JLRMethodParser.JLRMethod; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * Boot 1.x compatible request mapping implementation + * + * @author Kris De Volder + */ +public class RequestMapping1x extends AbstractRequestMapping { + + private JLRMethod methodData; + + private String path; + private String handler; + + protected RequestMapping1x(String path, String handler, TypeLookup typeLookup) { + super(typeLookup); + this.path = path; + this.handler = handler; + } + + @Override + public String getPath() { + return path; + } + + @Override + public String toString() { + return "RequestMapping1x("+path+")"; + } + + @Override + public String getFullyQualifiedClassName() { + JLRMethod m = getMethodData(); + if (m!=null) { + return m.getFQClassName(); + } + return null; + } + + /** + * Returns the raw string found in the requestmapping info. This is a 'toString' value + * of java.lang.reflect.Method object. + */ + public String getMethodString() { + try { + if (handler!=null) { + return handler; //Note: Boot 2.0 handler isn't always a method, but kind of hard to + // recognize. Since we don't do anything meaningfull if its not a method... + // its fine to treat everything as a method, as long as we don't make stuff + // 'explode' if we cannot find the corresponding method in classpath. + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + @Override + public String getMethodName() { + JLRMethod m = getMethodData(); + if (m!=null) { + return m.getMethodName(); + } + return null; + } + + protected JLRMethod getMethodData() { + if (methodData==null) { + methodData = JLRMethodParser.parse(getMethodString()); + } + return methodData; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((handler == null) ? 0 : handler.hashCode()); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RequestMapping1x other = (RequestMapping1x) obj; + if (handler == null) { + if (other.handler != null) + return false; + } else if (!handler.equals(other.handler)) + return false; + if (path == null) { + if (other.path != null) + return false; + } else if (!path.equals(other.path)) + return false; + return true; + } + + private static Stream processOrPaths(String pathExp) { + if (pathExp.contains("||")) { + String[] paths = pathExp.split(Pattern.quote("||")); + return Stream.of(paths).map(String::trim); + } else { + return Stream.of(pathExp); + } + } + + + private static String extractPath(String key) { + if (key.startsWith("{[")) { + //An almost json string. Unfortunately not really json so we can't + //use org.json or jackson Mapper to properly parse this. + int start = 2; //right after first '[' + int end = key.indexOf(']'); + if (end>=2) { + return key.substring(start, end); + } + } + //Case 1, or some unanticipated stuff. + //Assume the key is the path, which is right for Case 1 + // and probably more useful than null for 'unanticipated stuff'. + return key; + } + + public static Collection create(String predicate, String handler, TypeLookup typeLookup) { + return processOrPaths(extractPath(predicate)) + .map(path -> new RequestMapping1x(path, handler, typeLookup)) + .collect(Collectors.toList()); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMapping2x.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMapping2x.java new file mode 100644 index 000000000..3ed114fdc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMapping2x.java @@ -0,0 +1,188 @@ +/******************************************************************************* + * Copyright (c) 2018, 2020 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.model.actuator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.base.Objects; + +/** + * Boot 2.0 compatible request mapping implementation + * + * @author Alex Boyko + * + */ +public class RequestMapping2x extends AbstractRequestMapping { + + public RequestMapping2x(TypeLookup typeLookup, String path, String fqClassName, String methodName, String methodString) { + super(typeLookup); + this.path = path; + this.fqClassName = fqClassName; + this.methodName = methodName; + this.methodString = methodString; + } + + private String path; + private String fqClassName; + private String methodName; + private String methodString; + + @Override + public String getPath() { + return path; + } + + @Override + public String getFullyQualifiedClassName() { + return fqClassName; + } + + @Override + public String getMethodName() { + return methodName; + } + + @Override + public String getMethodString() { + return methodString; + } + + public static Collection createWebFlux(TypeLookup typeLookup, String predicate, JSONObject details) { + try { + if (predicate != null) { + // Predicate is and/or string expression: ((GET && /hello) && Accept: [text/plain]) + String[] tokens = predicate.split("\\w*(&&|\\|\\|)\\w*"); + List rms = new ArrayList<>(tokens.length); + for (String t : tokens) { + // Remove leading `(`, trailing `)` + String token = removeLeadingAndTrailingParenthises(t); + if (!token.isEmpty() && token.charAt(0) == '/') { + rms.add(new RequestMapping2x(typeLookup, token, null, null, null)); + } + } + return rms; + } + } catch (Exception e) { + Log.log(e); + } + return Collections.emptyList(); + } + + private static String removeLeadingAndTrailingParenthises(String s) { + int start = 0; + int end = s.length(); + for(; start < s.length() && (s.charAt(start) == '(' || Character.isWhitespace(s.charAt(start))); start++); + for(; end > start && (s.charAt(end - 1) == ')' || Character.isWhitespace(s.charAt(end - 1))); end--); + return start <= end ? s.substring(start, end) : ""; + } + + public static Collection create(TypeLookup typeLookup, String methodString, JSONObject details) { + try { + if (details != null) { + + JSONObject requestMappingConditionals = details.optJSONObject("requestMappingConditions"); + if (requestMappingConditionals != null) { + String[] paths = extractPaths(requestMappingConditionals.getJSONArray("patterns")); + String fqClassName = null; + String methodName = null; + if (details.has("handlerMethod")) { + JSONObject handlerMethod = details.getJSONObject("handlerMethod"); + fqClassName = handlerMethod.getString("className"); + methodName = handlerMethod.getString("name"); + } else if (details.has("handlerFunction")) { + fqClassName = details.getString("handlerFunction"); + int idx = fqClassName.indexOf("$$"); + if (idx >= 0) { + fqClassName = fqClassName.substring(0, idx); + } + } + final String clazz = fqClassName; + final String method = methodName; + return Arrays.stream(paths) + .map(path -> new RequestMapping2x(typeLookup, path, clazz, method, methodString)) + .collect(Collectors.toList()); + } + } + } catch (JSONException e) { + Log.log(e); + } + return Collections.emptyList(); + } + + public static Collection createFromSimpleServlet(TypeLookup typeLookup, + JSONObject servlet) { + try { + String[] extractPaths = extractPaths(servlet.getJSONArray("mappings")); + String className = servlet.getString("className"); + + if (extractPaths.length > 0) { + return Arrays.stream(extractPaths) + .filter(path -> path.length() > 0 && !path.equals("/")) + .map(path -> new RequestMapping2x(typeLookup, path, className, null, className)) + .collect(Collectors.toList()); + } + } catch (JSONException e) { + Log.log(e); + } + return Collections.emptyList(); + } + + private static String[] extractPaths(JSONArray pathArray) throws JSONException { + String[] paths = new String[pathArray.length()]; + for (int i = 0; i < pathArray.length(); i++) { + paths[i] = pathArray.getString(i); + } + return paths; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((methodString == null) ? 0 : methodString.hashCode()); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + result = prime * result + ((methodName == null) ? 0 : methodName.hashCode()); + result = prime * result + ((fqClassName == null) ? 0 : fqClassName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RequestMapping2x other = (RequestMapping2x) obj; + return Objects.equal(methodString, other.methodString) + && Objects.equal(fqClassName, other.fqClassName) + && Objects.equal(path, other.path) + && Objects.equal(methodName, other.methodName); + } + + @Override + public String toString() { + return "RequestMapping2x("+path+")"; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMappingsParser.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMappingsParser.java new file mode 100644 index 000000000..aabc5642e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/RequestMappingsParser.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2018 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.model.actuator; + +import java.util.List; + +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.ide.eclipse.beans.ui.live.model.TypeLookup; + +@FunctionalInterface +public interface RequestMappingsParser { + List parse(JSONObject obj, TypeLookup typeLookup) throws JSONException; +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/ActiveProfiles.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/ActiveProfiles.java new file mode 100644 index 000000000..23447efec --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/ActiveProfiles.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 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.model.actuator.env; + +import java.util.Collections; +import java.util.List; + +import org.springframework.ide.eclipse.beans.ui.live.model.DisplayName; + +import com.google.common.collect.ImmutableList; + +public class ActiveProfiles implements DisplayName { + + private final List profiles; + + public ActiveProfiles(List profiles) { + this.profiles = profiles != null ? profiles : Collections.emptyList(); + } + + @Override + public String getDisplayName() { + return "Active Profiles"; + } + + public List getProfiles() { + return ImmutableList.copyOf(this.profiles); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((profiles == null) ? 0 : profiles.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ActiveProfiles other = (ActiveProfiles) obj; + if (profiles == null) { + if (other.profiles != null) + return false; + } else if (!profiles.equals(other.profiles)) + return false; + return true; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/LiveEnvJsonParser1x.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/LiveEnvJsonParser1x.java new file mode 100644 index 000000000..6a6b4a6e1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/LiveEnvJsonParser1x.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) 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.model.actuator.env; + +import java.util.Iterator; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.ide.eclipse.beans.ui.live.model.JsonParser; + +import com.google.common.collect.ImmutableList; + +public class LiveEnvJsonParser1x implements JsonParser { + + public LiveEnvJsonParser1x() { + } + + @Override + public LiveEnvModel parse(String jsonInput) throws Exception { + JSONObject json = toJson(jsonInput); + ActiveProfiles profiles = parseProfiles(json); + PropertySources propertySources = parseProperties(json); + return new LiveEnvModel(profiles, propertySources); + } + + protected JSONObject toJson(String json) throws JSONException { + return new JSONObject(json); + } + + private ActiveProfiles parseProfiles(JSONObject envObj) { + Object _profiles = envObj.opt("profiles"); + ImmutableList.Builder list = ImmutableList.builder(); + + if (_profiles instanceof JSONArray) { + JSONArray profilesObj = (JSONArray) _profiles; + + for (int i = 0; i < profilesObj.length(); i++) { + Object object = profilesObj.opt(i); + if (object instanceof String) { + list.add(new Profile((String) object)); + } + } + } + List profiles = list.build(); + return new Profiles1x(profiles); + } + + /** + * + * @param envObj + * @return non-null PropertySources. Content in the PropertySources may be empty + * if no sources are found + * @throws Exception + */ + private PropertySources parseProperties(JSONObject allSourcesJson) throws Exception { + ImmutableList.Builder allSources = ImmutableList.builder(); + + if (allSourcesJson != null) { + Iterator keys = allSourcesJson.keys(); + if (keys != null) { + while (keys.hasNext()) { + Object key = keys.next(); + if (key instanceof String) { + String sourceName = (String) key; + // Skip profiles. It is parsed separately + if (!"profiles".equals(sourceName)) { + Object sourceObj = allSourcesJson.opt(sourceName); + PropertySource propertySource = new PropertySource(sourceName); + allSources.add(propertySource); + if (sourceObj instanceof JSONObject) { + JSONObject source = (JSONObject) sourceObj; + Iterator propKeys = source.keys(); + if (propKeys != null) { + ImmutableList.Builder parsedProps = ImmutableList.builder(); + while (propKeys.hasNext()) { + Object propObjKey = propKeys.next(); + if (propObjKey instanceof String) { + String propName = (String) propObjKey; + Object valObj = source.optString(propName); + if (valObj instanceof String) { + String value = (String) valObj; + Property property = new Property(propName, value, null); + parsedProps.add(property); + } + } + } + propertySource.add(parsedProps.build()); + } + } + } + } + } + } + } + + List sources = allSources.build(); + return new PropertySources(sources); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/LiveEnvJsonParser2x.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/LiveEnvJsonParser2x.java new file mode 100644 index 000000000..4348164d8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/LiveEnvJsonParser2x.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * Copyright (c) 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.model.actuator.env; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.ide.eclipse.beans.ui.live.model.JsonParser; +import org.springsource.ide.eclipse.commons.frameworks.core.util.StringUtils; + +import com.google.common.collect.ImmutableList; + +public class LiveEnvJsonParser2x implements JsonParser { + + public LiveEnvJsonParser2x() { + } + + @Override + public LiveEnvModel parse(String jsonInput) throws Exception { + + JSONObject envObj = toJson(jsonInput); + + ActiveProfiles profiles = parseActiveProfiles(envObj); + PropertySources propertySources = parseProperties(envObj); + + LiveEnvModel model = new LiveEnvModel(profiles, propertySources); + + return model; + } + + /** + * + * @param envObj + * @return non-null Active profiles. Content in the active profiles may be empty + * if no profiles are found + */ + private ActiveProfiles parseActiveProfiles(JSONObject envObj) { + Object _profiles = envObj.opt("activeProfiles"); + ImmutableList.Builder list = ImmutableList.builder(); + + if (_profiles instanceof JSONArray) { + JSONArray profilesObj = (JSONArray) _profiles; + + for (int i = 0; i < profilesObj.length(); i++) { + Object object = profilesObj.opt(i); + if (object instanceof String) { + list.add(new Profile((String) object)); + } + } + } + List profiles = list.build(); + return new ActiveProfiles(profiles); + } + + /** + * + * @param envObj + * @return non-null PropertySources. Content in the PropertySources may be empty + * if no sources are found + * @throws Exception + */ + private PropertySources parseProperties(JSONObject envObj) throws Exception { + ImmutableList.Builder allSources = ImmutableList.builder(); + + Object sourcesObj = envObj.opt("propertySources"); + + if (sourcesObj instanceof JSONArray) { + JSONArray props = (JSONArray) sourcesObj; + for (int i = 0; i < props.length(); i++) { + Object object = props.opt(i); + if (object instanceof JSONObject) { + JSONObject propObj = (JSONObject) object; + String name = propObj.optString("name"); + if (name != null) { + PropertySource propertySource = new PropertySource(name); + Object opt2 = propObj.opt("properties"); + List properties = parseProperties(opt2); + propertySource.add(properties); + + allSources.add(propertySource); + } + } + } + } + List sources = allSources.build(); + return new PropertySources(sources); + } + + private List parseProperties(Object opt2) { + List properties = new ArrayList<>(); + + if (opt2 instanceof JSONObject) { + JSONObject jsonObj = (JSONObject) opt2; + Iterator keys = jsonObj.keys(); + if (keys != null) { + while (keys.hasNext()) { + Object key = keys.next(); + if (key instanceof String) { + String propKey = (String) key; + Object propContentObj = jsonObj.opt(propKey); + if (propContentObj != null) { + properties.add(new Property(propKey, getValue(propContentObj), getOrigin(propContentObj))); + } + } + } + } + } + return properties; + } + + private PropertyOrigin getOrigin(Object propContentObj) { + if (propContentObj instanceof JSONObject) { + JSONObject jsonObj = (JSONObject) propContentObj; + String origin = jsonObj.optString("origin"); + if (StringUtils.hasText(origin)) { + return new PropertyOrigin(origin); + } + } + return null; + } + + private String getValue(Object propContentObj) { + if (propContentObj instanceof JSONObject) { + JSONObject jsonObj = (JSONObject) propContentObj; + return jsonObj.optString("value"); + } + return null; + } + + protected JSONObject toJson(String json) throws JSONException { + return new JSONObject(json); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/LiveEnvModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/LiveEnvModel.java new file mode 100644 index 000000000..71a2a0106 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/LiveEnvModel.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 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.model.actuator.env; + +import org.eclipse.core.runtime.Assert; + +public class LiveEnvModel implements Comparable { + + private final PropertySources propertySources; + private ActiveProfiles profiles; + + public LiveEnvModel(ActiveProfiles profiles, PropertySources propertySources) { + Assert.isNotNull(profiles); + Assert.isNotNull(propertySources); + this.profiles = profiles; + this.propertySources = propertySources; + } + + @Override + public int compareTo(LiveEnvModel o) { + return 0; + } + + public ActiveProfiles getActiveProfiles() { + return this.profiles; + } + + public PropertySources getPropertySources() { + return this.propertySources; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof LiveEnvModel) { + LiveEnvModel other = (LiveEnvModel) obj; + // Should be enough to compare contexts only since this is close to raw JSON data + return profiles.equals(other.profiles) && propertySources.equals(other.propertySources); + } + return super.equals(obj); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/Profile.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/Profile.java new file mode 100644 index 000000000..3e2e837e6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/Profile.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 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.model.actuator.env; + + +import org.springframework.ide.eclipse.beans.ui.live.model.DisplayName; + +public class Profile implements DisplayName { + + private final String name; + + public Profile(String name) { + this.name = name; + } + + @Override + public String getDisplayName() { + return name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Profile other = (Profile) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/Profiles1x.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/Profiles1x.java new file mode 100644 index 000000000..44897a37e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/Profiles1x.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 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.model.actuator.env; + +import java.util.List; + +public class Profiles1x extends ActiveProfiles { + + public Profiles1x(List profiles) { + super(profiles); + } + + @Override + public String getDisplayName() { + return "Profiles"; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/Property.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/Property.java new file mode 100644 index 000000000..66a52d533 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/Property.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 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.model.actuator.env; + +import org.springframework.ide.eclipse.beans.ui.live.model.DisplayName; + +public class Property implements DisplayName { + + private final String name; + private final String value; + private final PropertyOrigin origin; + + public Property(String name, String value, PropertyOrigin origin) { + this.name = name; + this.value = value; + this.origin = origin; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public PropertyOrigin getOrigin() { + return this.origin; + } + + @Override + public String getDisplayName() { + return this.name + " = " + this.value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((origin == null) ? 0 : origin.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Property other = (Property) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (origin == null) { + if (other.origin != null) + return false; + } else if (!origin.equals(other.origin)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/PropertyOrigin.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/PropertyOrigin.java new file mode 100644 index 000000000..8f45a861f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/PropertyOrigin.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 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.model.actuator.env; + +import org.springframework.ide.eclipse.beans.ui.live.model.DisplayName; + +public class PropertyOrigin implements DisplayName { + + private final String origin; + + public PropertyOrigin(String origin) { + this.origin = origin; + } + + @Override + public String getDisplayName() { + return "origin: " + getOrigin(); + } + + public String getOrigin() { + return origin; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((origin == null) ? 0 : origin.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PropertyOrigin other = (PropertyOrigin) obj; + if (origin == null) { + if (other.origin != null) + return false; + } else if (!origin.equals(other.origin)) + return false; + return true; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/PropertySource.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/PropertySource.java new file mode 100644 index 000000000..fa4d41da7 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/PropertySource.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 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.model.actuator.env; + +import java.util.Collections; +import java.util.List; + +import org.springframework.ide.eclipse.beans.ui.live.model.DisplayName; + +import com.google.common.collect.ImmutableList; + +public class PropertySource implements DisplayName { + + private final String name; + private List properties = Collections.emptyList(); + + public PropertySource(String name) { + this.name = name; + } + + @Override + public String getDisplayName() { + return name; + } + + public List getProperties() { + return ImmutableList.copyOf(properties); + } + + public void add(List properties) { + this.properties = properties != null ? properties : Collections.emptyList(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((properties == null) ? 0 : properties.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PropertySource other = (PropertySource) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (properties == null) { + if (other.properties != null) + return false; + } else if (!properties.equals(other.properties)) + return false; + return true; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/PropertySources.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/PropertySources.java new file mode 100644 index 000000000..4d943043a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/env/PropertySources.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 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.model.actuator.env; + +import java.util.Collections; +import java.util.List; + +import org.springframework.ide.eclipse.beans.ui.live.model.DisplayName; + +import com.google.common.collect.ImmutableList; + +public class PropertySources implements DisplayName { + + private final List propertySources; + + public PropertySources(List propertySources) { + this.propertySources = propertySources != null ? propertySources : Collections.emptyList(); + } + + public List getPropertySources() { + return ImmutableList.copyOf(propertySources); + } + + @Override + public String getDisplayName() { + return "Property Sources"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((propertySources == null) ? 0 : propertySources.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PropertySources other = (PropertySources) obj; + if (propertySources == null) { + if (other.propertySources != null) + return false; + } else if (!propertySources.equals(other.propertySources)) + return false; + return true; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/local/AutoCloudCliInstaller.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/local/AutoCloudCliInstaller.java new file mode 100644 index 000000000..78922a74c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/local/AutoCloudCliInstaller.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model.local; + +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.springframework.ide.eclipse.boot.core.cli.install.AutoInstallDescription; +import org.springframework.ide.eclipse.boot.core.cli.install.CloudCliInstall; +import org.springframework.ide.eclipse.boot.core.cli.install.IBootInstall; +import org.springframework.ide.eclipse.boot.core.cli.install.IBootInstallExtension; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +/** + * Helper class to perform an automatic installation of SprincCloud CLI into + * SpringBoot CLI. + */ +public class AutoCloudCliInstaller { + + private final IBootInstall bootInstall; + + public AutoCloudCliInstaller(IBootInstall bootInstall) { + super(); + this.bootInstall = bootInstall; + } + + public void performInstall(UserInteractions ui) { + AutoInstallDescription autoInstallation = bootInstall.checkAutoInstallable(CloudCliInstall.class); + if (!autoInstallation.isPossible) { + ui.errorPopup("Auto installation of Spring Cloud CLI not possible", autoInstallation.message); + } else if (ui.confirmOperation("Confirm Installation of Spring Cloud CLI?", autoInstallation.message)) { + InstallBootCliExtensionJob installCloudCliJob = new InstallBootCliExtensionJob("Auto install Spring Cloud CLI", CloudCliInstall.class); + installCloudCliJob.schedule(); + long waitTime = 200L; + for (long waited = 0L; installCloudCliJob.getState() != Job.NONE && waited < 30000L; waited+=waitTime) { + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + // ignore + } + } + if (installCloudCliJob.getState() != Job.NONE) { + Log.error("Timed out waiting for Spring Cloud CLI to be installed"); + } + } + } + + private class InstallBootCliExtensionJob extends Job { + + private Class extensionType; + + InstallBootCliExtensionJob(String name, Class extensionType) { + super(name); + this.extensionType = extensionType; + } + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + bootInstall.installExtension(extensionType); + return Status.OK_STATUS; + } catch (Exception e) { + return ExceptionUtil.status(e); + } + } + + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/local/LocalServicesModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/local/LocalServicesModel.java new file mode 100644 index 000000000..3828a7b12 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/local/LocalServicesModel.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * Copyright (c) 2017, 2020 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.model.local; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +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.springframework.ide.eclipse.boot.core.cli.install.CloudCliInstall; +import org.springframework.ide.eclipse.boot.core.cli.install.IBootInstall; +import org.springframework.ide.eclipse.boot.dash.model.BootDashHyperlink; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.ButtonModel; +import org.springframework.ide.eclipse.boot.dash.model.LocalBootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.LocalCloudServiceDashElement; +import org.springframework.ide.eclipse.boot.dash.model.RefreshState; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel.FilterChoice; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.util.version.Version; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +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.util.Log; + +/** + * This contain the bits and pieces of the LocalBootDashModel that pertain to local services. + * + * @author Alex Boyko - Orignal implementation + * @author Kris De Volder - Cleaned up code and separated from the rest, to make it easier to evolve. + */ +public class LocalServicesModel extends AbstractDisposable { + + private LocalBootDashModel bootDashModel; + private LiveExpression hideCloudCliServices; + private LiveVariable refreshState = new LiveVariable<>(RefreshState.READY); + private LiveSetVariable cloudCliServices = addDisposableChild(new LiveSetVariable<>(AsyncMode.SYNC)); + + private LiveSetVariable buttons = new LiveSetVariable<>(); + + BootDashHyperlink enableCloudServicesButton = new BootDashHyperlink("Install local cloud services") { + public void doPerform(UserInteractions ui) throws Exception { + IBootInstall bootInstall = defaultBootInstall.getValue(); + if (bootInstall!=null) { + if (bootInstall.getExtension(CloudCliInstall.class) == null) { + new AutoCloudCliInstaller(bootInstall).performInstall(ui); + } + if (bootInstall.getExtension(CloudCliInstall.class)!=null) { + viewerFilters.remove(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES); + } + } + } + }; + + private LiveSetVariable viewerFilters; + private LiveExpression defaultBootInstall; + private LiveExpression cloudCliInstall; + + public LocalServicesModel(BootDashViewModel viewModel, LocalBootDashModel bootDashModel, LiveExpression defaultBootInstall) { + this.defaultBootInstall = defaultBootInstall; + this.viewerFilters = viewModel.getToggleFilters().getSelectedFilters(); + this.bootDashModel = bootDashModel; + + hideCloudCliServices = addDisposableChild(new LiveExpression() { + { + dependsOn(viewerFilters); + } + + @Override + protected Boolean compute() { + return viewerFilters.contains(ToggleFiltersModel.FILTER_CHOICE_HIDE_LOCAL_SERVICES); + } + }); + cloudCliInstall = defaultBootInstall.then(bootInstall -> + bootInstall==null ? null : bootInstall.getExtensionExp(CloudCliInstall.class) + ); + cloudCliInstall.onChange(this, (e, v) -> { + if (cloudCliInstall.getValue()!=null) { + buttons.remove(enableCloudServicesButton); + } else { + buttons.add(enableCloudServicesButton); + } + refresh(); + }); + hideCloudCliServices.onChange(this, (e, v) -> { + refresh(); + }); + } + + @Override + public void dispose() { + super.dispose(); + if (cloudCliServices!=null) { + cloudCliServices.getValue().forEach(bde -> bde.dispose()); + cloudCliServices.dispose(); + cloudCliServices = null; + }; + } + + public void refresh() { + if (hideCloudCliServices.getValue()) { + cloudCliServices.getValue().forEach(bde -> bde.dispose()); + cloudCliServices.replaceAll(Collections.emptySet()); + buttons.replaceAll(Collections.emptySet()); + } else { + new Job("Loading local cloud services") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + if (cloudCliInstall.getValue() == null) { + buttons.add(enableCloudServicesButton); + } + + refreshState.setValue(RefreshState.loading("Fetching Local Cloud Sevices...")); + List newCloudCliservices = fetchLocalServices(); + cloudCliServices.getValue().forEach(bde -> bde.dispose()); + cloudCliServices.replaceAll(newCloudCliservices); + return Status.OK_STATUS; + } finally { + refreshState.setValue(RefreshState.READY); + } + } + }.schedule(); + } + } + + private List fetchLocalServices() { + IBootInstall bootInstall = defaultBootInstall.getValue(); + if (bootInstall!=null) { + try { + CloudCliInstall cloudCliInstall = bootInstall.getExtension(CloudCliInstall.class); + if (cloudCliInstall != null) { + Version cloudCliVersion = cloudCliInstall.getVersion(); + if (cloudCliVersion != null + && CloudCliInstall.CLOUD_CLI_JAVA_OPTS_SUPPORTING_VERSIONS.match(cloudCliVersion)) { + return Arrays.stream(cloudCliInstall.getCloudServices()).map(serviceId -> new LocalCloudServiceDashElement(bootDashModel, serviceId)).collect(Collectors.toList()); + } + } + } catch (Exception e) { + Log.log(e); + } + } + return Collections.emptyList(); + } + + public ObservableSet getCloudCliServices() { + return cloudCliServices; + } + + public LiveExpression getRefreshState() { + return refreshState; + } + + public ObservableSet getButtons() { + return buttons; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/AppDataSummarizer.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/AppDataSummarizer.java new file mode 100644 index 000000000..445ead19a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/AppDataSummarizer.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2020 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.model.remote; + +public interface AppDataSummarizer { + + /** + * Used to fetch data from the current node. This should only + * return the data that actually originiates at the node and not + * include data sumarised from its children. + */ + T getHere(GenericRemoteAppElement app); + + /** + * Used to fetch summary data from node. The summary accounts for + * the data from the node itself as well as the summarized data from + * all its children. + *

+ * THe typical implementation should call a method on the {@link GenericRemoteAppElement} + * that fetches the current value of a LiveExpression that computes the + * summary data. + */ + T getSummary(GenericRemoteAppElement element); + + /** + * Combines information from 2 data sources to create a 'summary' + * of both datas. + *

+ * Default implementation just keeps the first 'real' data + * and ignores the rest. + */ + T merge(T d1 , T d2); + + /** + * Defines the element that represents 'no data'. A typical value + * would be 'null' or '0'. + */ + default T zero() { + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/ChildBearing.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/ChildBearing.java new file mode 100644 index 000000000..c21023fb9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/ChildBearing.java @@ -0,0 +1,11 @@ +package org.springframework.ide.eclipse.boot.dash.model.remote; + +import java.util.List; + +import org.springframework.ide.eclipse.boot.dash.api.App; + +public interface ChildBearing { + + List fetchChildren() throws Exception; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/DebugLaunchTerminationListener.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/DebugLaunchTerminationListener.java new file mode 100644 index 000000000..9923f8035 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/DebugLaunchTerminationListener.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model.remote; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.tuple.Pair; +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.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchesListener2; +import org.springframework.ide.eclipse.boot.dash.model.RunState; + +import com.google.common.collect.ImmutableSet; + +/** + * Monitor launch termination and when a relevant launch is terminated, starts a polling loop to + * refresh related docker container. + */ +public class DebugLaunchTerminationListener implements ILaunchesListener2 { + + private static final boolean DEBUG = false; + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + private static final long GRACE_PERIOD = 10_000; + private static final long INTERVAL = 200; + + List> watched = new ArrayList<>(); + + /** + * THe 'deadline' is the moment in time at at which point we stop the polling loop that + * periodically refreshes elements belonging to watched terminated launches. + *

+ * The deadline is reset whenever something 'interesting' happens. This allows for + * some lag bewteen the triggering event (a launch was terminated) and the moment + * when we will actually be able to observe this reflected in the state of + * docker container. + */ + long deadline; + + synchronized public void add(ILaunch l, GenericRemoteAppElement e) { + watched.add(Pair.of(l,e)); + } + + public DebugLaunchTerminationListener() { + DebugPlugin.getDefault().getLaunchManager().addLaunchListener(this); + } + + @Override + public void launchesAdded(ILaunch[] arg0) { + //Don't care + debug("launches added"); + } + + @Override + public void launchesChanged(ILaunch[] arg0) { + debug("launches changed"); + //Don't care + } + + @Override + public void launchesRemoved(ILaunch[] arg0) { + debug("launches removed"); + //Don't care ?? + } + + @Override + public void launchesTerminated(ILaunch[] _terminateds) { + debug("launches terminated"); + if (isInteresting(_terminateds)) { + scheduleRefresh(); + } + } + + Job refresh = new Job("Refresh DockerContainers States") { + { + setSystem(true); + } + @Override + protected IStatus run(IProgressMonitor arg0) { + debug("Refreshing docker containers..."); + List toRefresh = new ArrayList<>(); + synchronized (refreshables) { + Iterator iter = refreshables.iterator(); + while (iter.hasNext()) { + GenericRemoteAppElement refreshable = iter.next(); + debug(refreshable+" = "+refreshable.getRunState()); + if (refreshable.isDisposed()) { + debug("skip refresh: was disposed: "+refreshable); + iter.remove(); + } else if (RunState.INACTIVE.equals(refreshable.getRunState())) { + debug("skip refresh: is already INACTIVE "+refreshable); + iter.remove(); + } else { + debug("will refresh "+refreshable); + toRefresh.add(refreshable); + } + } + long timeleft = deadline - System.currentTimeMillis(); + if (!refreshables.isEmpty() && timeleft>0) { + debug("trigger another round of refreshes"); + schedule(INTERVAL); + } else { + debug("ending refresh with "+refreshables.size() +" refreshables left"); + refreshables.clear(); + } + } + for (GenericRemoteAppElement e : toRefresh) { + Object _parent = e.getParent(); + if (_parent instanceof GenericRemoteAppElement) { + GenericRemoteAppElement parent = (GenericRemoteAppElement) _parent; + parent.getChildren().refresh(); + } + } + return Status.OK_STATUS; + } + }; + private Set refreshables = new HashSet<>(); + + private void scheduleRefresh() { + deadline = System.currentTimeMillis() + GRACE_PERIOD; + refresh.schedule(); + } + + private boolean isInteresting(ILaunch[] _terminateds) { + synchronized (refreshables) { + boolean interesting = false; + ImmutableSet terminateds = ImmutableSet.copyOf(_terminateds); + Iterator> iter = watched.iterator(); + while (iter.hasNext()) { + Pair w = iter.next(); + if (terminateds.contains(w.getKey())) { + refreshables.add(w.getValue()); + iter.remove(); + interesting = true; + } + } + return interesting; + } + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/GenericRemoteAppElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/GenericRemoteAppElement.java new file mode 100644 index 000000000..d13c6e416 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/GenericRemoteAppElement.java @@ -0,0 +1,901 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model.remote; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleListener; +import org.eclipse.ui.console.IConsoleManager; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansModel; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.api.ActualInstanceCount; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.AppConsole; +import org.springframework.ide.eclipse.boot.dash.api.AppContext; +import org.springframework.ide.eclipse.boot.dash.api.DebuggableApp; +import org.springframework.ide.eclipse.boot.dash.api.Deletable; +import org.springframework.ide.eclipse.boot.dash.api.DesiredInstanceCount; +import org.springframework.ide.eclipse.boot.dash.api.DevtoolsConnectable; +import org.springframework.ide.eclipse.boot.dash.api.JmxConnectable; +import org.springframework.ide.eclipse.boot.dash.api.LogConnection; +import org.springframework.ide.eclipse.boot.dash.api.LogSource; +import org.springframework.ide.eclipse.boot.dash.api.PortConnectable; +import org.springframework.ide.eclipse.boot.dash.api.ProjectRelatable; +import org.springframework.ide.eclipse.boot.dash.api.RunStateIconProvider; +import org.springframework.ide.eclipse.boot.dash.api.RunStateProvider; +import org.springframework.ide.eclipse.boot.dash.api.Styleable; +import org.springframework.ide.eclipse.boot.dash.api.SystemPropertySupport; +import org.springframework.ide.eclipse.boot.dash.console.CloudAppLogManager; +import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsUtil; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveDataCapableElement; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveDataConnectionManagementActions.ExecuteCommandAction; +import org.springframework.ide.eclipse.boot.dash.livexp.DisposingFactory; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.Failable; +import org.springframework.ide.eclipse.boot.dash.model.MissingLiveInfoMessages; +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.WrappingBootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.actuator.ActuatorClient; +import org.springframework.ide.eclipse.boot.dash.model.actuator.JMXActuatorClient; +import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.LiveEnvModel; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; +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.ui.Stylers; +import org.springsource.ide.eclipse.commons.livexp.util.Log; +import org.springsource.ide.eclipse.commons.livexp.util.OldValueDisposer; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; + +public class GenericRemoteAppElement extends WrappingBootDashElement implements Deletable, AppContext, Styleable, ElementStateListener, JmxConnectable, LiveDataCapableElement { + + private static final boolean DEBUG = false; + + private static AtomicInteger instances = new AtomicInteger(); + + private LiveVariable app = new LiveVariable<>(); + + private final Object parent; + + private LiveSetVariable existingChildIds = new LiveSetVariable<>(); + + private final RefreshStateTracker refreshTracker = new RefreshStateTracker(this); + + final protected IPropertyStore backingStore; + + final private List> summaries = new ArrayList<>(); + + DisposingFactory childFactory = new DisposingFactory(existingChildIds) { + @Override + protected GenericRemoteAppElement create(String appId) { + GenericRemoteAppElement parent = GenericRemoteAppElement.this; + GenericRemoteAppElement element = new GenericRemoteAppElement(getBootDashModel(), parent, appId, backingStore); + return element; + } + }; + + @Override + public ImageDescriptor getCustomRunStateIcon() { + App data = app.getValue(); + if (data instanceof RunStateIconProvider) { + ImageDescriptor icon = ((RunStateIconProvider) data).getRunStateIcon(getRunState()); + if (icon!=null) { + return icon; + } + } + return super.getCustomRunStateIcon(); + } + + private ObservableSet children = ObservableSet.builder().refresh(AsyncMode.ASYNC).compute(() -> { + + App appVal = app.getValue(); + if (appVal instanceof ChildBearing) { + try { + List children = ((ChildBearing)appVal).fetchChildren(); + ImmutableSet.Builder existingIds = ImmutableSet.builder(); + for (App app : children) { + existingIds.add(app.getName()); + } + existingChildIds.replaceAll(existingIds.build()); + + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (App child : children) { + GenericRemoteAppElement childElement = childFactory.createOrGet(child.getName()); + if (childElement!=null) { + child.setContext(childElement); + childElement.setAppData(child); + builder.add(childElement); + } else { + Log.warn("No boot dash element for child: "+child); + } + } + return builder.build(); +// } catch (TimeoutException e) { +// //ignore, expected error + } catch (Exception e) { + Log.log(e); + } + } + return ImmutableSet.of(); + }).build(); + { + children.dependsOn(app); + children.dependsOn(refreshTracker.refreshState); + } + + @Override + public void setGoalState(RunState s) { + App data = getAppData(); + if (data!=null) { + if (s==RunState.PAUSED) { + RemoteJavaLaunchUtil.disconnectRelatedLaunches(this); + } + data.setGoalState(s); + } + } + + LiveExpression baseRunState = new AsyncLiveExpression(RunState.UNKNOWN) { + { + dependsOn(app); + dependsOn(children); + } + + @Override + protected RunState compute() { + App data = app.getValue(); + Assert.isLegal(!(data instanceof RunStateProvider && data instanceof ChildBearing)); + if (data instanceof RunStateProvider) { + RunState v = ((RunStateProvider) data).fetchRunState(); + return v; + } else if (data instanceof ChildBearing) { + RunState v = RunState.INACTIVE; + for (BootDashElement child : children.getValues()) { + v = v.merge(child.getRunState()); + } + return v; + } + return RunState.UNKNOWN; + } + }; + + private JmxRunStateTracker jmxRunStateTracker = new JmxRunStateTracker(this, baseRunState, app); + + + private ObservableSet livePorts = ObservableSet.builder().refresh(AsyncMode.ASYNC).compute(() -> { + + ImmutableSet.Builder builder = ImmutableSet.builder(); + + if (getRunState() == RunState.RUNNING || getRunState() == RunState.DEBUGGING) { + App appVal = app.getValue(); + debug("appVal: " + appVal); + if (appVal instanceof PortConnectable) { + + Set appLivePorts = ((PortConnectable) appVal).getPorts(); + + debug("from PortConnectable: " +appLivePorts); + + if (appLivePorts != null) { + builder.addAll(appLivePorts); + } + } + else { + debug("not PortConnectable"); + } + + ImmutableSet children = this.children.getValue(); + if (children != null && !children.isEmpty()) { + for (BootDashElement child : children) { + ImmutableSet childPorts = child.getLivePorts(); + debug("from child: " + child.getName() + " - " + childPorts); + if (childPorts != null) { + builder.addAll(childPorts); + } + } + } + else { + debug("No Children"); + } + } + return builder.build(); + }).build(); + + { + livePorts.dependsOn(children); + livePorts.dependsOn(app); + livePorts.dependsOn(getRunStateExp()); + } + + private LiveExpression debugPort = new AsyncLiveExpression(0) { + + //it is important that events for this exp are fired asynchronously to avoid starving ui thread + // causing deadlock. (I.e. the important thing is to have the event listeners not be doing things + // in the ui thread. It might be okay to do the refresh of the exp itself synchronously. + { + dependsOn(app); + } + protected Integer compute() { + App data = app.getValue(); + if (data instanceof DebuggableApp) { + return ((DebuggableApp) data).getDebugPort(); + } + return 0; + }; + }; + + private LiveExpression remoteDevtoolsUrl = new AsyncLiveExpression(null) { + + //it is important that events for this exp are fired asynchronously to avoid starving ui thread + // causing deadlock. (I.e. the important thing is to have the event listeners not be doing things + // in the ui thread. It might be okay to do the refresh of the exp itself synchronously. + + { + dependsOn(livePorts); + } + + protected String compute() { + return DevtoolsUtil.remoteUrl(GenericRemoteAppElement.this); + } + }; + + private LiveExpression actualInstanceCounts = sumarizeFromChildren(new AppDataSummarizer() { + + public Integer zero() { return 0;} + + @Override + public Integer getHere(GenericRemoteAppElement app) { + if (getRunState().isActive()) { + App appVal = app.getAppData(); + debug("appVal: " + appVal); + if (appVal instanceof ActualInstanceCount) { + return ((ActualInstanceCount) appVal).getActualInstances(); + } + } + return zero(); + } + + @Override + public Integer getSummary(GenericRemoteAppElement element) { + return element.getActualInstances(); + } + + public Integer merge(Integer d1, Integer d2) { + return d1 + d2; + } + }); + + private OldValueDisposer logConnection = new OldValueDisposer<>(this); + + private LiveExpression sumarizeFromChildren(AppDataSummarizer sumarizer) { + AsyncLiveExpression sumary = new AsyncLiveExpression(sumarizer.zero()) { + @Override + protected T compute() { + T sum = sumarizer.getHere(GenericRemoteAppElement.this); + ImmutableSet children = GenericRemoteAppElement.this.children.getValue(); + if (children != null && !children.isEmpty()) { + for (BootDashElement _child : children) { + if (_child instanceof GenericRemoteAppElement) { + GenericRemoteAppElement child = ((GenericRemoteAppElement) _child); + sum = sumarizer.merge(sum, sumarizer.getSummary(child)); + } + } + } + return sum; + } + }; + sumary.dependsOn(children); + sumary.dependsOn(app); + sumary.dependsOn(getRunStateExp()); + addDisposableChild(sumary); + addElementNotifier(sumary); + summaries.add(sumary); + return sumary; + } + + @Override + public RefreshState getRefreshState() { + return refreshTracker.refreshState.getValue(); + } + + private void debug(String message) { + if (DEBUG) { + System.out.println(this + ": " + message); + } + } + + public GenericRemoteAppElement(GenericRemoteBootDashModel model, Object parent, String appId, IPropertyStore parentPropertyStore) { + super(model, appId); + backingStore = PropertyStores.createSubStore(getName(), parentPropertyStore); + children.dependsOn(model.refreshCount()); + addDisposableChild(children); + addElementNotifier(children); + + baseRunState.dependsOn(model.refreshCount()); + addDisposableChild(this.childFactory); + this.parent = parent; + + app.dependsOn(model.getRunTarget().getClientExp()); + app.dependsOn(getBootDashModel().refreshCount()); + addDisposableChild(baseRunState); + addDisposableChild(jmxRunStateTracker); + addElementNotifier(getRunStateExp()); + addElementNotifier(refreshTracker.refreshState); + addDisposableChild(livePorts); + addElementNotifier(livePorts); + + model.addElementStateListener(this); + + debugPort.onChange(this, (e, v) -> { + RemoteJavaLaunchUtil.synchronizeWith(this); + }); + + remoteDevtoolsUrl.onChange(this, (e, v) -> { + DevtoolsUtil.launchClientIfNeeded(this); + }); + + refreshTracker.refreshState.addListener((e, v) -> { + RefreshState s = e.getValue(); + if (s!=null && !s.isLoading()) { + app.refresh(); + } + }); + + onDispose(d -> { + model.removeElementStateListener(this); + }); + + getRunStateExp().onChange(this, (e, v) -> connectOrDisconnectConsoleIfNeeded()); + + IConsoleListener consoleListener = new IConsoleListener() { + + @Override + public void consolesAdded(IConsole[] consoles) { + connectOrDisconnectConsoleIfNeeded(); + } + + @Override + public void consolesRemoved(IConsole[] consoles) { + connectOrDisconnectConsoleIfNeeded(); + } + + }; + + IConsoleManager consoleManager = injections().getBean(CloudAppLogManager.class).getConsoleManager(); + + consoleManager.addConsoleListener(consoleListener); + + addDisposableChild(() -> consoleManager.removeConsoleListener(consoleListener)); + } + + private boolean firstConsoleConnection = true; + private void connectOrDisconnectConsoleIfNeeded() { + App app = this.app.getValue(); + CloudAppLogManager appLogManager = injections().getBean(CloudAppLogManager.class); + LogConnection logConnection = this.logConnection.getVar().getValue(); + boolean hasConnection = logConnection != null && !logConnection.isClosed(); + boolean connectConsole = app instanceof LogSource && appLogManager.hasConsole(app); + + if (hasConnection != connectConsole) { + if (connectConsole) { + AppConsole console = appLogManager.getConsole(app); + this.logConnection.setValue(((LogSource)app).connectLog(console, firstConsoleConnection)); + firstConsoleConnection = false; + } else { + this.logConnection.setValue(null); + } + } + + } + + @Override + public GenericRemoteBootDashModel getBootDashModel() { + return (GenericRemoteBootDashModel) super.getBootDashModel(); + } + + @Override + public String getName() { + return super.delegate; + } + + public App getAppData() { + return this.app.getValue(); + } + + public void setAppData(App appData) { + this.app.setValue(appData); + } + + @Override + public RunState getRunState() { + return getRunStateExp().getValue(); + } + + private LiveExpression getRunStateExp() { + return jmxRunStateTracker.augmentedRunState; + } + + @Override + public RefreshStateTracker getRefreshTracker() { + return refreshTracker; + } + + @Override + public EnumSet supportedGoalStates() { + App app = this.app.getValue(); + return app!=null?app.supportedGoalStates():EnumSet.noneOf(RunState.class); + } + + @Override + public IProject getProject() { + App data = this.app.getValue(); + if (data instanceof ProjectRelatable) { + return ((ProjectRelatable) data).getProject(); + } + return null; + } + + @Override + public int getLivePort() { + ImmutableSet ports = getLivePorts(); + if (ports != null && !ports.isEmpty()) { + return ports.iterator().next(); + } + return 0; + } + + @Override + public ImmutableSet getLivePorts() { + return livePorts.getValue(); + } + + @Override + public String getLiveHost() { + return "localhost"; + } + + @Override + public ILaunchConfiguration getActiveConfig() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void stop() throws Exception { + App a = this.app.getValue(); + a.setGoalState(RunState.INACTIVE); + } + + @Override + public void restart(RunState runingOrDebugging, UserInteractions ui) throws Exception { + App a = this.app.getValue(); + a.restart(runingOrDebugging); + } + + @Override + public void openConfig(UserInteractions ui) { + // TODO Auto-generated method stub + + } + + @Override + public int getActualInstances() { + return actualInstanceCounts.getValue(); + } + + @Override + public int getDesiredInstances() { + App data = getAppData(); + if (data instanceof DesiredInstanceCount) { + return ((DesiredInstanceCount)data).getDesiredInstances(); + } + return -1; + } + + @Override + public Object getParent() { + return parent; + } + + @Override + public PropertyStoreApi getPersistentProperties() { + return PropertyStores.createApi(backingStore); + } + + @Override + public ObservableSet getChildren() { + return children; + } + + @Override + public boolean canDelete() { + App app = this.app.getValue(); + if (app instanceof Deletable) { + return ((Deletable) app).canDelete(); + } + return false; + } + + @Override + public void delete() throws Exception { + App app = this.app.getValue(); + if (app instanceof Deletable) { + ((Deletable) app).delete(); + } + } + + @Override + public StyledString getStyledName(Stylers stylers) { + App app = this.app.getValue(); + if (app instanceof Styleable) { + return ((Styleable) app).getStyledName(stylers); + } + return new StyledString(getName()); + } + + @Override + public void stateChanged(BootDashElement e) { + this.livePorts.refresh(); + for (LiveExpression sumary : summaries) { + sumary.refresh(); + } + //We do the next thing asynchronously because stateChanged events can be fired on the ui + // thread and the 'synchronizeWith' can a) take a while and b) cause deadlock. +// refreshTracker.runAsync("Synchronize remote java launch", () -> { +// System.out.println("refresh java debug launch"); +// RemoteJavaLaunchUtil.synchronizeWith(this); +// }); +// refreshTracker.runAsync("Synchronize devtools client launch", () -> { +// DevtoolsUtil.launchClientIfNeeded(this); +// }); + } + + /** + * Summarise all the debug ports either defined by this node or it's children. + */ + public ImmutableSet getDebugPortSummary() { + ImmutableSet.Builder ports = ImmutableSet.builder(); + collectDebugPorts(ports); + return ports.build(); + } + + private void collectDebugPorts(Builder ports) { + int port = getDebugPort(); + if (port>0) { + ports.add(port); + } + for (BootDashElement c : children.getValues()) { + if (c instanceof GenericRemoteAppElement) { + ((GenericRemoteAppElement)c).collectDebugPorts(ports); + } + } + } + + @Override + public ImmutableSet getLaunchConfigs() { + return RemoteJavaLaunchUtil.getLaunchConfigs(this); + } + + public int getDebugPort() { + return debugPort.getValue(); + } + + @Override + public String getJmxUrl() { + App data = getAppData(); + if (data!=null && data instanceof JmxConnectable) { + return ((JmxConnectable)data).getJmxUrl(); + } + return null; + } + + public boolean canWriteToConsole() { + App data = getAppData(); + return data instanceof LogSource; + } + + private LiveExpression> actuatorUrls = sumarizeFromChildren(new AppDataSummarizer>() { + + @Override + public Set getHere(GenericRemoteAppElement app) { + String url = app.getActuatorUrlHere(); + return url == null ? ImmutableSet.of() : ImmutableSet.of(url); + } + + @Override + public Set merge(Set d1, Set d2) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + if (d1 != null) { + builder.addAll(d1); + } + if (d2 != null) { + builder.addAll(d2); + } + return builder.build(); + } + + @Override + public Set getSummary(GenericRemoteAppElement element) { + return element.getActuatorUrls().getValue(); + } + + }); + public LiveExpression> getActuatorUrls() { + return actuatorUrls; + } + + String getActuatorUrlHere() { + RunState rs = getRunState(); + if (rs.isActive()) { + return getJmxUrl(); + } + return null; + } + + private LiveExpression>> liveRequestMappings; + private LiveExpression> liveEnv; + private LiveExpression> liveBeans; + + @Override + public Failable> getLiveRequestMappings() { + synchronized (this) { + if (liveRequestMappings==null) { + final LiveExpression> actuatorUrls = getActuatorUrls(); + liveRequestMappings = new AsyncLiveExpression>>(Failable.error(MissingLiveInfoMessages.NOT_YET_COMPUTED), "Fetch request mappings for '"+getStyledName(null).getString()+"'") { + @Override + protected Failable> compute() { + Set targets = actuatorUrls.getValue(); + if (targets!=null && !targets.isEmpty()) { + if (targets.size() == 1) { + String target = targets.iterator().next(); + ActuatorClient client = JMXActuatorClient.forUrl(getTypeLookup(), () -> target); + List list = client.getRequestMappings(); + if (list!=null) { + return Failable.of(ImmutableList.copyOf(client.getRequestMappings())); + } + } else { + return Failable.error(buffer -> buffer.p("More than one child can provide live data. Please select one.")); + } + } + return Failable.error(getBootDashModel().getRunTarget().getType().getMissingLiveInfoMessages().getMissingInfoMessage(getStyledName(null).getString(), "mappings")); + } + + }; + liveRequestMappings.dependsOn(actuatorUrls); + addElementState(liveRequestMappings); + addDisposableChild(liveRequestMappings); + } + } + return liveRequestMappings.getValue(); + } + + @Override + public Failable getLiveEnv() { + synchronized (this) { + if (liveEnv == null) { + final LiveExpression> actuatorUrls = getActuatorUrls(); + liveEnv = new AsyncLiveExpression>(Failable.error(MissingLiveInfoMessages.NOT_YET_COMPUTED), "Fetch env for '"+getStyledName(null).getString()+"'") { + @Override + protected Failable compute() { + Set targets = actuatorUrls.getValue(); + if (targets!=null && !targets.isEmpty()) { + if (targets.size() == 1) { + String target = targets.iterator().next(); + ActuatorClient client = JMXActuatorClient.forUrl(getTypeLookup(), () -> target); + LiveEnvModel env = client.getEnv(); + if (env != null) { + return Failable.of(env); + } + } else { + return Failable.error(buffer -> buffer.p("More than one child can provide live data. Please select one.")); + } + } + return Failable.error(getBootDashModel().getRunTarget().getType().getMissingLiveInfoMessages().getMissingInfoMessage(getStyledName(null).getString(), "env")); + } + + }; + liveEnv.dependsOn(actuatorUrls); + addElementState(liveEnv); + addDisposableChild(liveEnv); + } + } + return liveEnv.getValue(); + } + + protected final SimpleDIContext injections() { + return getBootDashModel().getViewModel().getContext().injections; + } + + @Override + public Failable getLiveBeans() { + synchronized (this) { + if (liveBeans == null) { + LiveExpression> actuatorUrls = getActuatorUrls(); + liveBeans = new AsyncLiveExpression>(Failable.error(MissingLiveInfoMessages.NOT_YET_COMPUTED), "Fetch beans for '"+getStyledName(null).getString()+"'") { + @Override + protected Failable compute() { + Set targets = actuatorUrls.getValue(); + if (targets!=null && !targets.isEmpty()) { + if (targets.size() == 1) { + String target = targets.iterator().next(); + ActuatorClient client = JMXActuatorClient.forUrl(getTypeLookup(), () -> target); + LiveBeansModel beans = client.getBeans(); + if (beans != null) { + return Failable.of(beans); + } + } else { + return Failable.error(buffer -> buffer.p("More than one child can provide live data. Please select one.")); + } + } + return Failable.error(getBootDashModel().getRunTarget().getType().getMissingLiveInfoMessages().getMissingInfoMessage(getStyledName(null).getString(), "beans")); + } + + }; + liveBeans.dependsOn(actuatorUrls); + addElementState(liveBeans); + addDisposableChild(liveBeans); + } + } + return liveBeans.getValue(); + } + + public CompletableFuture enableDevtools(boolean enable) { + if (enable) { + return refreshTracker.runAsync("Enable Devtools Support for application '" + getStyledName(null).getString() + "'", () -> { + App app = getAppData(); + if (app instanceof SystemPropertySupport) { + SystemPropertySupport sysprops = (SystemPropertySupport) app; + IProject project = getProject(); + if (project!=null) { + sysprops.setSystemProperty(DevtoolsUtil.REMOTE_SECRET_PROP, DevtoolsUtil.getSecret(project)); + } + } + }); + } else { + return refreshTracker.runAsync("Disable Devtools Support for application '" + getStyledName(null).getString() + "'", () -> { + App app = getAppData(); + if (app instanceof SystemPropertySupport) { + SystemPropertySupport sysprops = (SystemPropertySupport) app; + sysprops.setSystemProperty(DevtoolsUtil.REMOTE_SECRET_PROP, null); + } + }); + } + } + + private static class LaunchTerminator implements DisposeListener, ValueListener { + + private ILaunch launch; + private GenericRemoteAppElement owner; + + public LaunchTerminator(ILaunch launch, GenericRemoteAppElement owner) { + this.launch = launch; + this.owner = owner; + owner.getRunStateExp().addListener(this); + owner.onDispose(d -> { + terminate(); + }); + + } + + @Override + public void gotValue(LiveExpression exp, RunState value) { + if (exp.getValue()==RunState.INACTIVE) { + terminate(); + } + } + + @Override + public void disposed(Disposable disposed) { + terminate(); + } + + void terminate() { + ILaunch launch; + synchronized (this) { + launch = this.launch; + this.launch = null; + } + if (launch!=null) { + try { + launch.terminate(); + } catch (Exception e) { + Log.log(e); + } + owner.getRunStateExp().removeListener(this); + } + } + } + + public void restartRemoteDevtoolsClient() { + refreshTracker.runAsync("(Re)starting remote devtools client", () -> { + IProject project = getProject(); + if (project!=null) { + DevtoolsUtil.disconnectDevtoolsClientsFor(this); + ILaunch launch = DevtoolsUtil.launchDevtools(this, DevtoolsUtil.getSecret(project), ILaunchManager.RUN_MODE, new NullProgressMonitor()); + new LaunchTerminator(launch, this); + } + }); + } + + public UserInteractions ui() { + return getBootDashModel().ui(); + } + + @Override + public ImageDescriptor getRunStateImageDecoration() { + if (this.getTarget() != null && this.getRunState() == RunState.RUNNING) { + if (DevtoolsUtil.isDevClientAttached(this, ILaunchManager.RUN_MODE)) { + return BootDashActivator.getDefault().getImageRegistry().getDescriptor(BootDashActivator.DT_ICON_ID); + } + } + return null; + } + + @Override + public boolean hasDevtoolsDependency() { + App data = getAppData(); + if (data instanceof DevtoolsConnectable) { + return ((DevtoolsConnectable) data).hasDevtoolsDependency(); + } + return super.hasDevtoolsDependency(); + } + + @Override + public boolean isDevtoolsGreenColor() { + App data = getAppData(); + if (data instanceof DevtoolsConnectable) { + return ((DevtoolsConnectable) data).getDevtoolsSecret()!=null; + } + return false; + } + + @Override + public String getConsoleDisplayName() { + App data = app.getValue(); + return data!=null ? data.getConsoleDisplayName() : null; + } + + @Override + public boolean matchesLiveProcessCommand(ExecuteCommandAction action) { + App data = getAppData(); + return data.getName() != null&& data.getName().equals(action.getProcessId()); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/GenericRemoteAppElementDataContributor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/GenericRemoteAppElementDataContributor.java new file mode 100644 index 000000000..822e50b15 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/GenericRemoteAppElementDataContributor.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model.remote; + +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springsource.ide.eclipse.commons.boot.ls.remoteapps.RemoteBootAppsDataHolder.Contributor; +import org.springsource.ide.eclipse.commons.boot.ls.remoteapps.RemoteBootAppsDataHolder.RemoteAppData; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +import com.google.common.collect.ImmutableSet; + +public class GenericRemoteAppElementDataContributor implements Contributor, ElementStateListener { + + private ObservableSet remoteApps; + + public GenericRemoteAppElementDataContributor(SimpleDIContext injections) { + BootDashViewModel bdv = injections.getBean(BootDashViewModel.class); + bdv.addElementStateListener(this); + this.remoteApps = ObservableSet.builder().refresh(AsyncMode.ASYNC) + .compute(() -> { + ImmutableSet.Builder allApps = ImmutableSet.builder(); + ImmutableSet models = bdv.getSectionModels().getValue(); + for (BootDashModel bootDashModel : models) { + if (bootDashModel instanceof GenericRemoteBootDashModel) { + ObservableSet childrenExp = bootDashModel.getElements(); + collectFrom(childrenExp, allApps); + } + } + return allApps.build(); + }) + .build(); + remoteApps.setRefreshDelay(2_000); //This delay should not be necessary, but... + // there seems to be a race condition in spring-boot-ls v2 live hover connection mechanics. + // when updates to the remote apps fire in quick succession, it often starts shutting down + // the connection but then doesn't reconnect again when the same url is added again while + // the disconnect is still in progress. + // Adding refresh delay avoids this issue by never sending updates in quick succession + remoteApps.onChange((e, v) -> { + System.out.println(">>> remote jmx apps"); + for (RemoteAppData remoteAppData : remoteApps.getValues()) { + System.out.println(remoteAppData); + } + System.out.println("<<< remote jmx apps"); + }); + remoteApps.refresh(); //need one initial manual refresh because registering for + // boot dash element changes only triggers when something changes. + } + + private void collectFrom(ObservableSet childrenExp, ImmutableSet.Builder allApps) { + for (BootDashElement child : childrenExp.getValues()) { + if (child instanceof GenericRemoteAppElement) { + collectFrom((GenericRemoteAppElement)child, allApps); + } + } + } + + private void collectFrom(GenericRemoteAppElement child, ImmutableSet.Builder allApps) { + String actuatorUrl = child.getActuatorUrlHere(); + if (actuatorUrl!=null) { + RemoteAppData data = new RemoteAppData(actuatorUrl, child.getLiveHost()); + data.setUrlScheme("http"); + data.setPort(""+child.getLivePort()); + data.setKeepChecking(false); + data.setProcessId(child.getAppData().getName()); + data.setProcessName(child.getConsoleDisplayName()); + allApps.add(data); + } + collectFrom(child.getChildren(), allApps); + } + + @Override + public ObservableSet getRemoteApps() { + return remoteApps; + } + + @Override + public void stateChanged(BootDashElement e) { + remoteApps.refresh(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/GenericRemoteBootDashModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/GenericRemoteBootDashModel.java new file mode 100644 index 000000000..bfb651036 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/GenericRemoteBootDashModel.java @@ -0,0 +1,193 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model.remote; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.jdt.core.IJavaProject; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.Deletable; +import org.springframework.ide.eclipse.boot.dash.api.ProjectDeploymentTarget; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.RemoteBootDashModel; +import org.springframework.ide.eclipse.boot.dash.livexp.DisposingFactory; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +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.RemoteRunTarget; +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.ObservableSet; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; + +public class GenericRemoteBootDashModel extends RemoteBootDashModel { + + private LiveSetVariable existingAppIds = new LiveSetVariable<>(); + + DisposingFactory elementFactory = new DisposingFactory(existingAppIds) { + @Override + protected GenericRemoteAppElement create(String appId) { + return new GenericRemoteAppElement(GenericRemoteBootDashModel.this, GenericRemoteBootDashModel.this, appId, + GenericRemoteBootDashModel.this.getPropertyStore()); + } + }; + + private final ObservableSet elements; + + public GenericRemoteBootDashModel(RemoteRunTarget target, BootDashViewModel parent) { + super(target, parent); + elements = ObservableSet.builder() + .refresh(AsyncMode.ASYNC) + .compute(() -> fetchApps()) + .build(); + elements.dependsOn(getRunTarget().getClientExp()); + addElementStateListener(element -> refresh(ui())); + } + + private ImmutableSet fetchApps() throws Exception { + return refreshTracker.call("Fetching apps...", () -> { + Collection apps = getRunTarget().fetchApps(); + Set validAppIds = new HashSet<>(); + for (App app : apps) { + validAppIds.add(app.getName()); + } + existingAppIds.replaceAll(validAppIds); + Builder bdes = ImmutableSet.builder(); + for (App app : apps) { + GenericRemoteAppElement bde = elementFactory.createOrGet(app.getName()); + app.setContext(bde); + bde.setAppData(app); + bdes.add(bde); + } + return bdes.build(); + }); + } + + @Override + public ObservableSet getElements() { + return elements; + } + + @Override + public void refresh(UserInteractions ui) { + elements.refresh(); + } + + @SuppressWarnings("unchecked") + @Override + public RemoteRunTarget getRunTarget() { + return (RemoteRunTarget) super.getRunTarget(); + } + + public LiveExpression refreshCount() { + return elements.refreshCount(); + } + + @Override + public RefreshState getRefreshState() { + return refreshTracker.refreshState.getValue(); + } + + @Override + public void delete(Collection collection, UserInteractions ui) { + List> futures = new ArrayList<>(); + for (BootDashElement d : collection) { + if (d instanceof Deletable) { + futures.add(refreshTracker.callAsync("Deleting "+d.getName(), () -> { + try { + ((Deletable) d).delete(); + } catch (Exception e) { + Log.log(e); + } + return null; + })); + } + } + for (CompletableFuture f : futures) { + try { + f.get(); + } catch (Exception e) { + Log.log(e); + } + } + refresh(ui()); + } + + @Override + public boolean canDelete(BootDashElement element) { + if (element instanceof Deletable) { + Deletable re = (Deletable) element; + return re.canDelete(); + } + return false; + } + + @Override + public String getDeletionConfirmationMessage(Collection value) { + return null; // no confirmation asked. + } + + @Override + public boolean canBeAdded(List sources) { + if (getRunTarget() instanceof ProjectDeploymentTarget) { + 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; + } + + 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; + } + + @Override + public void performDeployment(Set toDeploy, RunState runOrDebug) throws Exception { + if (!toDeploy.isEmpty()) { + RemoteRunTarget _target = getRunTarget(); + if (_target instanceof ProjectDeploymentTarget) { + ProjectDeploymentTarget target = (ProjectDeploymentTarget) _target; + refreshTracker.run("Creating deployment", () -> { + target.performDeployment(toDeploy, runOrDebug); + }); + refresh(ui()); + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/JmxRunStateTracker.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/JmxRunStateTracker.java new file mode 100644 index 000000000..586735bc1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/JmxRunStateTracker.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model.remote; + +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeoutException; + +import javax.management.remote.JMXConnector; + +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.JmxConnectable; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.launch.util.JMXClient; +import org.springframework.ide.eclipse.boot.launch.util.SpringApplicationLifeCycleClientManager; +import org.springframework.ide.eclipse.boot.launch.util.SpringApplicationLifecycleClient; +import org.springsource.ide.eclipse.commons.livexp.core.AbstractDisposable; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public class JmxRunStateTracker extends AbstractDisposable { + + private final GenericRemoteAppElement bde; + private final LiveExpression _baseRunState; + private final Callable connectionProvider; + private SpringApplicationLifeCycleClientManager clientMgr; + + private static final long APP_STARTUP_TIMEOUT = 60_000; + private static final long POLLING_INTERVAL = 500; + private static final boolean DEBUG = false; + + long creationTime = System.currentTimeMillis(); + + + private void debug(String string) { + if (DEBUG) { + System.out.println(this+" : "+string); + } + } + + public final LiveExpression augmentedRunState = new AsyncLiveExpression(RunState.INACTIVE) { + + + { + setRefreshDelay(POLLING_INTERVAL); + } + + @Override + protected RunState compute() { + debug("Computing augmented runstate..."); + RunState baseRunState = _baseRunState.getValue(); + debug("baseRunState = "+baseRunState); + if (baseRunState.isActive()) { + Exception error = null; + try { + SpringApplicationLifecycleClient client = clientMgr.getLifeCycleClient(); + if (client==null || client.isReady()) { + debug("jmxClient.isReady() => true"); + return baseRunState; + } + //client.isReady() => false + } catch (Exception e) { + // failed to connect + error = e; + clientMgr.disposeClient(); + } + // failed to connect or client.isReady -> false + try { + refreshMaybe(error); + return RunState.STARTING; + } catch (Exception e1) { + Throwable cause = ExceptionUtil.getDeepestCause(e1); + if (cause instanceof IOException || cause instanceof TimeoutException) { + //expected when container goes away, connections dropping / breaking etc. + } else { + Log.log(e1); + } + return RunState.UNKNOWN; + } + } else { + return baseRunState; + } + } + + + private void refreshMaybe(Exception error) throws Exception { + if (!isDisposed()) { + long age = System.currentTimeMillis()-creationTime; + debug("age = "+ age); + if (age < APP_STARTUP_TIMEOUT) { + refresh(); + } else { + if (error != null) { + throw error; + } + else { + throw new TimeoutException(); + } + } + } + } + }; + + public JmxRunStateTracker(GenericRemoteAppElement bde, LiveExpression baseRunState, LiveExpression app) { + this.bde = bde; + this._baseRunState = baseRunState; + this.connectionProvider = () -> { + App data = app.getValue(); + if (data instanceof JmxConnectable) { + String url = ((JmxConnectable) data).getJmxUrl(); + debug("jmxUrl = "+url); + if (url!=null) { + return JMXClient.createJmxConnectorFromUrl(url); + } + } + return null; + }; + this.clientMgr = new SpringApplicationLifeCycleClientManager(connectionProvider); + _baseRunState.onChange(this, (_e, _v) -> { + creationTime = System.currentTimeMillis(); + augmentedRunState.refresh(); + }); + augmentedRunState.dependsOn(app); + onDispose(d -> clientMgr.disposeClient()); + } + + @Override + public String toString() { + return "JmxRunStateTracker("+bde+")"; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/RefreshStateTracker.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/RefreshStateTracker.java new file mode 100644 index 000000000..7db4e07a3 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/RefreshStateTracker.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * Copyright (c) 2020 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.model.remote; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.runtime.Assert; +import org.springframework.ide.eclipse.boot.dash.model.RefreshState; +import org.springframework.ide.eclipse.boot.dash.util.RunnableWithException; +import org.springsource.ide.eclipse.commons.frameworks.core.util.JobUtil; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.OnDispose; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public class RefreshStateTracker { + + private static Map> debugObservers = null; + + public static void clearDebugObservers() { + debugObservers = null; + } + public static CompletableFuture waitForOperation(String message) { + if (debugObservers==null) { + debugObservers = new HashMap<>(); + } + return debugObservers.computeIfAbsent(message, s -> new CompletableFuture<>()); + } + + private CompletableFuture getObserver(String message) { + if (debugObservers!=null) { + return debugObservers.get(message); + } + return null; + } + + private final Map map = new HashMap<>(); + + private final Set inProgress = new HashSet<>(); + + public RefreshStateTracker(OnDispose owner) { + owner.onDispose(d -> refreshState.dispose()); + } + + public final LiveExpression refreshState = new LiveExpression(RefreshState.READY) { + protected RefreshState compute() { + RefreshState merged = RefreshState.READY; + synchronized (RefreshStateTracker.this) { + for (RefreshState s : map.values()) { + merged = RefreshState.merge(merged, s); + } + } + return merged; + } + }; + + public void run(String busyMessage, RunnableWithException runner) throws Exception { + call(busyMessage, () -> { + runner.run(); + return null; + }); + } + + public T call(String busyMessage, Callable callable) throws Exception { + start(busyMessage); + try { + T success = callable.call(); + success(busyMessage); + return success; + } catch (Throwable e) { + if (ExceptionUtil.isWarning(e)) { + warn(busyMessage, ExceptionUtil.getMessage(e)); + } else { + Log.log(e); + error(busyMessage, ExceptionUtil.getMessage(e)); + } + throw ExceptionUtil.exception(e); + } + } + + private void warn(String busyMessage, String warningMessage) { + Assert.isLegal(inProgress.remove(busyMessage)); + map.put(busyMessage, RefreshState.warning(warningMessage)); + refreshState.refresh(); + } + + private synchronized void error(String busyMessage, String errorMessage) { + Assert.isLegal(inProgress.remove(busyMessage)); + map.put(busyMessage, RefreshState.error(errorMessage)); + refreshState.refresh(); + } + + private synchronized void success(String busyMessage) { + Assert.isLegal(inProgress.remove(busyMessage)); + map.remove(busyMessage); + refreshState.refresh(); + } + + private synchronized void start(String busyMessage) { + Assert.isLegal(inProgress.add(busyMessage)); + map.put(busyMessage, RefreshState.loading(busyMessage)); + refreshState.refresh(); + } + + public CompletableFuture callAsync(String busyMessage, Callable callable) { + CompletableFuture observer = getObserver(busyMessage); + CompletableFuture result = new CompletableFuture<>(); + JobUtil.runInJob(busyMessage, (mon) -> { + try { + result.complete(this.call(busyMessage, callable)); + if (observer!=null) { + observer.complete(null); + } + } catch (Throwable e) { + result.completeExceptionally(e); + if (observer!=null) { + observer.complete(null); + } + } + }); + return result; + } + + public CompletableFuture runAsync(String busyMessage, RunnableWithException runnable) { + return callAsync(busyMessage, () -> { + runnable.run(); + return null; + }); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/RemoteJavaLaunchUtil.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/RemoteJavaLaunchUtil.java new file mode 100644 index 000000000..6e5955f4e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/remote/RemoteJavaLaunchUtil.java @@ -0,0 +1,212 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model.remote; + +import java.util.Collection; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.Launch; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jdt.launching.JavaRuntime; +import org.springframework.ide.eclipse.boot.util.RetryUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +public class RemoteJavaLaunchUtil { + + public static final String DISABLE_HCR_LAUNCH_ATTRIBUTE = "org.eclipse.jdt.debug.disable.hcr"; //$NON-NLS-1$ + //should be same value as org.eclipse.jdt.debug.core.JDIDebugModel.DISABLE_HCR_LAUNCH_ATTRIBUTE + //We don't refer to that constant directly because it will only exist in Eclipse 4.18 (assuming PR is accepted) + //See: https://git.eclipse.org/r/c/jdt/eclipse.jdt.debug/+/169601 + //In the mean time, we can already set this attrbute, it will do no harm (has no effect). + + + public static void cleanupOldLaunchConfigs(Collection existinginElements) { + //TODO: implement this and find a good place and time to call it from. + } + + public static final String APP_NAME = "sts.boot.dash.element.name"; + + /** + * Check the state of a remote boot dash element and whether it needs to have a debugger + * attached. If yes, make sure there is a debugger attached. + */ + public synchronized static void synchronizeWith(GenericRemoteAppElement app) { + if (isDebuggable(app)) { + ILaunch l = ensureDebuggerAttached(app); + if (app.hasDevtoolsDependency()) { + l.setAttribute(DISABLE_HCR_LAUNCH_ATTRIBUTE, "true"); + } + if (l!=null) { + terminationListener().add(l, app); + } + } + } + + /** + * When containerized apps are being 'suspended' while being debugged the debug connections + * get confused. So 'Pause' action should disconnect debuggers from these apps. + * @param app + */ + public static void disconnectRelatedLaunches(GenericRemoteAppElement app) { + ILaunchManager lm = DebugPlugin.getDefault().getLaunchManager(); + for (ILaunch _l : lm.getLaunches()) { + try { + if (_l instanceof Launch) { + Launch l = (Launch) _l; + ILaunchConfiguration conf = l.getLaunchConfiguration(); + if (conf!=null) { + if (app.getName().equals(conf.getAttribute(APP_NAME, ((String)null)))) { + if (!l.isTerminated()) { + if (((Launch)l).canDisconnect()) { + l.disconnect(); + } + } + } + } + } + } catch (Exception e) { + Log.log(e); + } + } + } + + private static DebugLaunchTerminationListener terminationListener; + + private synchronized static DebugLaunchTerminationListener terminationListener() { + if (terminationListener==null) { + terminationListener = new DebugLaunchTerminationListener(); + DebugPlugin.getDefault().getLaunchManager().addLaunchListener(terminationListener); + } + return terminationListener; + } + + private static ILaunch ensureDebuggerAttached(GenericRemoteAppElement app) { + try { + ILaunchConfiguration conf = getLaunchConfig(app); + if (conf==null) { + conf = createLaunchConfig(app); + } + return ensureActiveLaunch(conf); + } catch (Exception e) { + Log.log(e); + } + return null; + } + + + private static ILaunchConfiguration createLaunchConfig(GenericRemoteAppElement app) throws CoreException { + ILaunchManager lm = DebugPlugin.getDefault().getLaunchManager(); + String launchConfName = lm.generateLaunchConfigurationName(app.getStyledName(null).getString()); + + ILaunchConfigurationWorkingCopy wc = remoteJavaType().newInstance(null, launchConfName); + wc.setAttribute(APP_NAME, app.getName()); + wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_ALLOW_TERMINATE, true); + wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_VM_CONNECTOR, JavaRuntime.getDefaultVMConnector().getIdentifier()); + + wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, app.getProject().getName()); + + wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_CONNECT_MAP, ImmutableMap.of( + "hostname", "localhost", + "port", ""+app.getDebugPort() + )); + return wc.doSave(); + } + + private static ILaunch ensureActiveLaunch(ILaunchConfiguration conf) throws Exception { + ILaunchManager lm = DebugPlugin.getDefault().getLaunchManager(); + for (ILaunch l : lm.getLaunches()) { + if (conf.equals(l.getLaunchConfiguration())) { + if (!l.isTerminated()) { + return null; + } + } + }; + return RetryUtil.retry(50, 500, () -> conf.launch(ILaunchManager.DEBUG_MODE, new NullProgressMonitor(), false, true)); + } + + private static ILaunchConfiguration getLaunchConfig(GenericRemoteAppElement app) { + try { + ILaunchManager lm = DebugPlugin.getDefault().getLaunchManager(); + ILaunchConfigurationType remoteJavaType = remoteJavaType(); + ILaunchConfiguration[] configs = lm.getLaunchConfigurations(remoteJavaType); + for (ILaunchConfiguration conf : configs) { + if (app.getName().equals(getAppName(conf))) { + return conf; + } + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + + private static ILaunchConfigurationType remoteJavaType() { + ILaunchManager lm = DebugPlugin.getDefault().getLaunchManager(); + ILaunchConfigurationType remoteJavaType = lm.getLaunchConfigurationType(IJavaLaunchConfigurationConstants.ID_REMOTE_JAVA_APPLICATION); + return remoteJavaType; + } + + + private static String getAppName(ILaunchConfiguration conf) { + try { + return conf.getAttribute(APP_NAME, (String)null); + } catch (CoreException e) { + Log.log(e); + } + return null; + } + + + private static boolean isDebuggable(GenericRemoteAppElement app) { + try { + int debugPort = app.getDebugPort(); + IProject project = app.getProject(); + return debugPort>0 && project!=null && project.isAccessible() && project.hasNature(JavaCore.NATURE_ID); + } catch (Exception e) { + Log.log(e); + } + return false; + } + + public static ImmutableSet getLaunchConfigs(GenericRemoteAppElement element) { + try { + String appName = element.getName(); + ILaunchManager lm = DebugPlugin.getDefault().getLaunchManager(); + ILaunchConfigurationType type = lm.getLaunchConfigurationType(IJavaLaunchConfigurationConstants.ID_REMOTE_JAVA_APPLICATION); + ILaunchConfiguration[] confs = lm.getLaunchConfigurations(type); + if (confs!=null) { + ImmutableSet.Builder found = ImmutableSet.builder(); + for (ILaunchConfiguration c : confs) { + if (appName.equals(c.getAttribute(APP_NAME, (String)null))) { + found.add(c); + } + } + return found.build(); + } + } catch (Exception e) { + Log.log(e); + } + return ImmutableSet.of(); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/AbstractRemoteRunTargetType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/AbstractRemoteRunTargetType.java new file mode 100644 index 000000000..d41592694 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/AbstractRemoteRunTargetType.java @@ -0,0 +1,15 @@ +package org.springframework.ide.eclipse.boot.dash.model.runtargettypes; + +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; + +public abstract class AbstractRemoteRunTargetType extends AbstractRunTargetType implements RemoteRunTargetType { + + public AbstractRemoteRunTargetType(SimpleDIContext injections, String name) { + super(injections, name); + } + + @Override + public final boolean canInstantiate() { + return true; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/AbstractRunTargetType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/AbstractRunTargetType.java new file mode 100644 index 000000000..63d9f747c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/AbstractRunTargetType.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * Copyright (c) 2015 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.model.runtargettypes; + +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModelContext; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; + +/** + * @author Kris De Volder + */ +public abstract class AbstractRunTargetType implements RunTargetType { + + private static final String NAME_TEMPLATE = "NAME_TEMPLATE"; + + protected final SimpleDIContext injections; + private String name; + private IPropertyStore propertyStore; + + public AbstractRunTargetType(SimpleDIContext injections, String name) { + this.name = name; + this.injections = injections; + //TODO: there shouldn't be any exceptions to allow for target types that don't provide a context. + // However this requires a bunch of refactoring to get rid of the global constants related to the + // 'LOCAL' runtarget and type. + if (injections!=null) { + BootDashModelContext context = injections.getBean(BootDashModelContext.class); + this.propertyStore = PropertyStores.createSubStore(name, context.getViewProperties()); + } + } + + @Override + public String getName() { + return name; + } + + @Override + public String toString() { + return "RunTargetType("+getName()+")"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AbstractRunTargetType other = (AbstractRunTargetType) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + @Override + public IPropertyStore getPropertyStore() { + return propertyStore; + } + + @Override + public PropertyStoreApi getPersistentProperties() { + IPropertyStore store = getPropertyStore(); + if (store!=null) { + return new PropertyStoreApi(store); + } + return null; + } + + @Override + public String getDefaultNameTemplate() { + return null; + } + + @Override + public void setNameTemplate(String template) throws Exception { + getPersistentProperties().put(NAME_TEMPLATE, template); + } + + @Override + public String getNameTemplate() { + PropertyStoreApi props = getPersistentProperties(); + if (props!=null) { + String customTemplate = props.get(NAME_TEMPLATE); + if (customTemplate!=null) { + return customTemplate; + } + } + return getDefaultNameTemplate(); + } + + @Override + public String getTemplateHelpText() { + return null; + } + + protected UserInteractions ui() { + return injections.getBean(UserInteractions.class); + } + + public final SimpleDIContext injections() { + return injections; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/CannotAccessPropertyException.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/CannotAccessPropertyException.java new file mode 100644 index 000000000..bd5cd056e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/CannotAccessPropertyException.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model.runtargettypes; + +public class CannotAccessPropertyException extends Exception { + + private Exception cause; + + private static final long serialVersionUID = 6564016896619132677L; + + public CannotAccessPropertyException(String message, Exception cause) { + super(message); + this.cause = cause; + } + + public Exception getCause() { + return cause; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/LocalRunTargetType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/LocalRunTargetType.java new file mode 100644 index 000000000..6028b7654 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/LocalRunTargetType.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.model.runtargettypes; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; + +/** + * @author Kris De Volder + */ +public class LocalRunTargetType extends AbstractRunTargetType { + LocalRunTargetType(String name) { + super(null, name); + } + + @Override + public boolean canInstantiate() { + return false; + } + + public String toString() { + return "RunTargetType(LOCAL)"; + } + + @Override + public CompletableFuture openTargetCreationUi(LiveSetVariable targets) { + throw new UnsupportedOperationException( + this + " is a Singleton, it is not possible to create additional targets of this type."); + } + + @Override + public RunTarget createRunTarget(Void nothing) { + throw new UnsupportedOperationException(); + } + + @Override + public ImageDescriptor getIcon() { + return BootDashActivator.getDefault().getImageRegistry().getDescriptor(BootDashActivator.BOOT_ICON); + } + + @Override + public Void parseParams(String serializedTargetParams) { + throw new UnsupportedOperationException(); + } + + @Override + public String serialize(Void serializedTargetParams) { + throw new UnsupportedOperationException(); + } + + @Override + public ImageDescriptor getDisconnectedIcon() { + return getIcon(); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/RemoteRunTarget.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/RemoteRunTarget.java new file mode 100644 index 000000000..bb991dc7a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/RemoteRunTarget.java @@ -0,0 +1,51 @@ +package org.springframework.ide.eclipse.boot.dash.model.runtargettypes; + +import java.util.Collection; + +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteBootDashModel; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +public interface RemoteRunTarget extends RunTarget { + + public enum ConnectMode { + INTERACTIVE, // connect operation directly triggered by user action. In this case popping up dialogs to ask for + // information such as password is okay + AUTOMATIC // connect operation triggered during startup to re-establish connection to a persisted target. + // in this case the implementation should avoid popping up dialogs. Instead if not sufficient info + // was persisted, then the connect operation siltently aborts / does nothing. + } + + default boolean isConnected() { + return getClientExp().getValue()!=null; + } + LiveExpression getClientExp(); + default Client getClient() { + return getClientExp().getValue(); + } + + /** + * Typically long-running (network access), avoid calling in UI thread). + */ + Collection fetchApps() throws Exception; + + /** + * Disconnects the remote target and removes/disposes its + * client. + */ + void disconnect(); + + /** + * Attempts to connects to remote target. When succesful a 'client' + * is created in the process and this client is subsequently available + * as the value of the getClient LiveExp. + *

+ * May throw an exception signaling that something went wrong + * trying to connect. + * @param mode + */ + void connect(ConnectMode mode) throws Exception; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/RemoteRunTargetType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/RemoteRunTargetType.java new file mode 100644 index 000000000..484470182 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/RemoteRunTargetType.java @@ -0,0 +1,17 @@ +/******************************************************************************* + * Copyright (c) 2020 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.model.runtargettypes; + +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +public interface RemoteRunTargetType

extends RunTargetType

{ +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/RunTargetTypes.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/RunTargetTypes.java new file mode 100644 index 000000000..dab691839 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/RunTargetTypes.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2015 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.model.runtargettypes; + +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; + +public class RunTargetTypes { + + //TODO: Get rid of the 'LOCAL' contstants in this class. + // The existence of this class and all the references littered around the + // code pointing to its constants makes it rather hard in some cases to + // mock things out for unit testing. + public static final RunTargetType LOCAL = new LocalRunTargetType("Local"); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/TargetProperties.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/TargetProperties.java new file mode 100644 index 000000000..8c9ccf399 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/runtargettypes/TargetProperties.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.model.runtargettypes; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModelContext; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; + +/** + * Properties for a particular {@link RunTarget}. + *

+ * Properties define two things: + *

+ * 1. Copy of all properties + *

+ * 2. Properties that should be persisted + *

+ * Target properties should be associated with both a {@link RunTargetType} and + * a {@link RunTarget} id. + * + */ +public class TargetProperties { + + public static final String RUN_TARGET_ID = "runTargetID"; + + protected Map map; + protected RunTargetType type; + + public TargetProperties(Map map, RunTargetType type) { + this.map = map; + this.type = type; + } + + public TargetProperties() { + this(new HashMap(), null); + } + + public TargetProperties(TargetProperties targetProperties, RunTargetType runTargetType) { + this( + targetProperties==null + ?new HashMap<>() + :targetProperties.getAllProperties(), + runTargetType + ); + } + + public TargetProperties(RunTargetType type) { + this(new HashMap(), type); + } + + public TargetProperties(RunTargetType type, String runTargetId, BootDashModelContext context) { + this(new HashMap(), type); + put(RUN_TARGET_ID, runTargetId); + } + + public TargetProperties(Map map, RunTargetType type, String runTargetId) { + this(type); + if (map != null) { + this.map = map; + } + put(RUN_TARGET_ID, runTargetId); + } + + public String get(String property) { + return map.get(property); + } + + /** + * + * @return properties that should be persisted. May be a subset of all the + * properties from {@link #getAllProperties()} + */ + public Map getPropertiesToPersist() { + return getAllProperties(); + } + + /** + * @return non-null map of properties. This is a copy of the actual map + */ + public Map getAllProperties() { + return new HashMap<>(map); + } + + public String getRunTargetId() { + return map.get(RUN_TARGET_ID); + } + + public RunTargetType getRunTargetType() { + return type; + } + + public void put(String key, String value) { + if (value == null) { + map.remove(key); + } else { + map.put(key, value); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKClient.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKClient.java new file mode 100644 index 000000000..db0aafec9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKClient.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.ngrok; + +import java.io.IOException; +import java.net.URLEncoder; + +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.fluent.Request; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * @author Martin Lippert + */ +public class NGROKClient { + + private static int CREATE_TUNNEL_TIMEOUT_SECONDS = 6; + + private String path; + + private NGROKProcess process; + private NGROKTunnel tunnel; + + public NGROKClient(String path) { + this.path = path; + + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + if (process != null) { + process.terminate(); + } + } + })); + } + + public NGROKTunnel getTunnel() { + return tunnel; + } + + public String getURL() { + return process != null ? process.getApiURL() : null; + } + + public NGROKTunnel startTunnel(String proto, String addr) throws Exception { + process = NGROKProcess.startNGROK(path, proto, addr); + if (process != null) { + + boolean success = false; + int seconds = 0; + + while (!success && seconds < CREATE_TUNNEL_TIMEOUT_SECONDS) { + NGROKTunnel[] tunnels = retrieveTunnels(); + if (tunnels != null && tunnels.length > 0) { + for (int i = 0; i < tunnels.length; i++) { + if (tunnels[i].getAddr().endsWith(addr) && tunnels[i].getProto().equals(proto)) { + tunnel = tunnels[i]; + return tunnel; + } + } + } + seconds++; + Thread.sleep(1000); + } + } + + return null; + } + + public void shutdown() { + if (tunnel != null) { + shutdownTunnel(tunnel); + tunnel = null; + } + + if (process != null) { + process.terminate(); + process = null; + } + } + + private NGROKTunnel[] retrieveTunnels() { + NGROKTunnel[] result = null; + try { + String response = Request.Get(process.getApiURL() + "/api/tunnels").execute().returnContent().asString(); + + JSONObject jsonResponse = new JSONObject(response); + + JSONArray tunnels = jsonResponse.getJSONArray("tunnels"); + if (tunnels != null) { + result = new NGROKTunnel[tunnels.length()]; + for (int i = 0; i < result.length; i++) { + JSONObject tunnel = tunnels.getJSONObject(i); + String name = tunnel.getString("name"); + String proto = tunnel.getString("proto"); + String public_url = tunnel.getString("public_url"); + String addr = tunnel.getJSONObject("config").getString("addr"); + + result[i] = new NGROKTunnel(name, proto, public_url, addr); + } + } + } + catch (Exception e) { + System.out.println(e); + // do nothing, might be the case that the ngrok process is not yet up + } + + return result; + } + + private void shutdownTunnel(NGROKTunnel ngrokTunnel) { + try { + String deleteURL = process.getApiURL() + "/api/tunnels/" + URLEncoder.encode(ngrokTunnel.getName(), "UTF-8"); + HttpResponse response = Request.Delete(deleteURL).execute().returnResponse(); + if (response.getStatusLine().getStatusCode() != 204) { + System.err.println("errro closing tunnel"); + } + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKInstallBlock.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKInstallBlock.java new file mode 100644 index 000000000..58b22eebf --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKInstallBlock.java @@ -0,0 +1,445 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.ngrok; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.eclipse.core.runtime.ListenerList; +import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.jface.viewers.CheckStateChangedEvent; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.ICheckStateListener; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.ISelectionProvider; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerComparator; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.util.SWTFactory; + + +/** + * @author Christian Dupuis + * @author Steffen Pingel + * @author Kris De Volder + */ +public class NGROKInstallBlock implements ISelectionProvider { + + private static final int defaultColumnWidth = 350; + + class NGROKContentProvider implements IStructuredContentProvider { + public void dispose() { + } + + public Object[] getElements(Object input) { + return ngroks.toArray(); + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + } + + class NGROKLabelProvider extends LabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + public String getColumnText(Object element, int columnIndex) { + return element.toString(); + } + + } + + private Composite control; + private final List ngroks = new ArrayList(); + private CheckboxTableViewer ngrokList; + private Button addButton; + private Button removeButton; + private Button editButton; + + private final ListenerList selectionListeners = new ListenerList(); + private ISelection prevSelection = new StructuredSelection(); + private Table table; + + private NGROKInstallManager installManager; + + public NGROKInstallBlock(NGROKInstallManager installManager) { + this.installManager = installManager; + } + + public void addSelectionChangedListener(ISelectionChangedListener listener) { + selectionListeners.add(listener); + } + + public void createControl(Composite ancestor) { + Font font = ancestor.getFont(); + Composite parent = SWTFactory.createComposite(ancestor, font, 2, 1, GridData.FILL_BOTH); + control = parent; + + SWTFactory.createLabel(parent, "ngrok Installations:", 2); + + table = new Table(parent, SWT.CHECK | SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION); + GridData gd = new GridData(GridData.FILL_BOTH); + gd.heightHint = 250; + gd.widthHint = 350; + table.setLayoutData(gd); + table.setFont(font); + table.setHeaderVisible(true); + table.setLinesVisible(true); + + TableColumn column = new TableColumn(table, SWT.NULL); + column.setText("Location"); + column.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + sortByLocation(); + } + }); + + ngrokList = new CheckboxTableViewer(table); + ngrokList.setLabelProvider(new NGROKLabelProvider()); + ngrokList.setContentProvider(new NGROKContentProvider()); + // by default, sort by name + sortByLocation(); + + ngrokList.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent evt) { + enableButtons(); + } + }); + + ngrokList.addCheckStateListener(new ICheckStateListener() { + public void checkStateChanged(CheckStateChangedEvent event) { + if (event.getChecked()) { + setCheckedNGROK((String) event.getElement()); + } + else { + setCheckedNGROK(null); + } + } + }); + + ngrokList.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent e) { + if (!ngrokList.getSelection().isEmpty()) { + editNGROK(); + } + } + }); + table.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + if (event.character == SWT.DEL && event.stateMask == 0) { + if (removeButton.isEnabled()) { + removeNGROKs(); + } + } + } + }); + + Composite buttons = SWTFactory.createComposite(parent, font, 1, 1, GridData.VERTICAL_ALIGN_BEGINNING, 0, 0); + + addButton = SWTFactory.createPushButton(buttons, "Add...", null); + addButton.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event evt) { + addNGROK(); + } + }); + + editButton = SWTFactory.createPushButton(buttons, "Edit...", null); + editButton.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event evt) { + editNGROK(); + } + }); + + removeButton = SWTFactory.createPushButton(buttons, "Remove...", null); + removeButton.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event evt) { + removeNGROKs(); + } + }); + + SWTFactory.createVerticalSpacer(parent, 1); + + fillWithWorkspaceNGROKs(); + enableButtons(); + addButton.setEnabled(true); + } + + public String getCheckedNGROK() { + Object[] objects = ngrokList.getCheckedElements(); + if (objects.length == 0) { + return null; + } + return (String) objects[0]; + } + + public Control getControl() { + return control; + } + + public String[] getNGROKs() { + return ngroks.toArray(new String[ngroks.size()]); + } + + public ISelection getSelection() { + return new StructuredSelection(ngrokList.getCheckedElements()); + } + + public boolean isDuplicateName(String name) { + for (int i = 0; i < ngroks.size(); i++) { + String ngrok = ngroks.get(i); + if (ngrok.equals(name)) { + return true; + } + } + return false; + } + + public void removeNGROKs(String[] ngroksToBeRemoved) { + IStructuredSelection prev = (IStructuredSelection) getSelection(); + for (String ngrok : ngroksToBeRemoved) { + ngroks.remove(ngrok); + } + ngrokList.refresh(); + + IStructuredSelection curr = (IStructuredSelection) getSelection(); + if (!curr.equals(prev)) { + String[] installs = getNGROKs(); + if (curr.size() == 0 && installs.length == 1) { + setSelection(new StructuredSelection(installs[0])); + } + else { + fireSelectionChanged(); + } + } + } + + public void removeSelectionChangedListener(ISelectionChangedListener listener) { + selectionListeners.remove(listener); + } + + public void restoreColumnSettings(IDialogSettings settings, String qualifier) { + ngrokList.getTable().layout(true); + restoreColumnWidths(settings, qualifier); + } + + public void saveColumnSettings(IDialogSettings settings, String qualifier) { + int columnCount = table.getColumnCount(); + for (int i = 0; i < columnCount; i++) { + settings.put(qualifier + ".columnWidth" + i, table.getColumn(i).getWidth()); //$NON-NLS-1$ + } + } + + public void setCheckedNGROK(String vm) { + if (vm == null) { + setSelection(new StructuredSelection()); + } + else { + setSelection(new StructuredSelection(vm)); + } + } + + public void setSelection(ISelection selection) { + if (selection instanceof IStructuredSelection) { + if (!selection.equals(prevSelection)) { + prevSelection = selection; + Object ngrok = ((IStructuredSelection) selection).getFirstElement(); + if (ngrok == null) { + ngrokList.setCheckedElements(new Object[0]); + } + else { + ngrokList.setCheckedElements(new Object[] { ngrok }); + ngrokList.reveal(ngrok); + } + fireSelectionChanged(); + } + } + } + + public void ngrokAdded(String ngrok) { + ngroks.add(ngrok); + ngrokList.refresh(); + } + + private void addNGROK() { + try { + FileDialog fileDialog = new FileDialog(control.getShell()); + fileDialog.setText("select ngrok executable"); + + String result = fileDialog.open(); + if (result != null) { + ngroks.add(result); + ngrokList.refresh(); + ngrokList.setSelection(new StructuredSelection(result)); + setSelection(new StructuredSelection(result)); + } + } catch (Exception e) { + BootActivator.log(e); + } + } + + private void editNGROK() { + IStructuredSelection selection = (IStructuredSelection) ngrokList.getSelection(); + String ngrok = (String) selection.getFirstElement(); + if (ngrok == null) { + return; + } + + FileDialog fileDialog = new FileDialog(control.getShell()); + fileDialog.setText("select ngrok executable"); + fileDialog.setFileName(ngrok); + + String result = fileDialog.open(); + if (result != null) { + // replace with the edited VM + int index = ngroks.indexOf(ngrok); + ngroks.remove(index); + ngroks.add(index, result); + ngrokList.refresh(); + ngrokList.setSelection(new StructuredSelection(result)); + } + } + + private void enableButtons() { + IStructuredSelection selection = (IStructuredSelection) ngrokList.getSelection(); + + int selectionCount = selection.size(); + editButton.setEnabled(selectionCount == 1); + if (selectionCount > 0 && selectionCount < ngrokList.getTable().getItemCount()) { + removeButton.setEnabled(true); + } + else { + removeButton.setEnabled(false); + } + } + + private void fireSelectionChanged() { + SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection()); + Object[] listeners = selectionListeners.getListeners(); + for (Object listener2 : listeners) { + ISelectionChangedListener listener = (ISelectionChangedListener) listener2; + listener.selectionChanged(event); + } + } + + private void removeNGROKs() { + IStructuredSelection selection = (IStructuredSelection) ngrokList.getSelection(); + String[] ngroks = new String[selection.size()]; + Iterator iter = selection.iterator(); + + int i = 0; + while (iter.hasNext()) { + ngroks[i] = iter.next(); + i++; + } + removeNGROKs(ngroks); + } + + private void restoreColumnWidths(IDialogSettings settings, String qualifier) { + int columnCount = table.getColumnCount(); + for (int i = 0; i < columnCount; i++) { + int width = -1; + try { + width = settings.getInt(qualifier + ".columnWidth" + i); //$NON-NLS-1$ + } + catch (NumberFormatException e) { + } + + if (width <= 0) { + table.getColumn(i).setWidth(defaultColumnWidth); + } + else { + table.getColumn(i).setWidth(width); + } + } + } + + private void sortByLocation() { + ngrokList.setComparator(new ViewerComparator() { + @Override + public int compare(Viewer viewer, Object e1, Object e2) { + if ((e1 instanceof String) && (e2 instanceof String)) { + String left = (String) e1; + String right = (String) e2; + return left.compareToIgnoreCase(right); + } + return super.compare(viewer, e1, e2); + } + + @Override + public boolean isSorterProperty(Object element, String property) { + return true; + } + }); + } + + protected void fillWithWorkspaceNGROKs() { + Collection installs = installManager.getInstalls(); + String deflt = null; + try { + deflt = installManager.getDefaultInstall(); + } catch (Exception e) { + BootActivator.log(e); + } + setNGROKs(installs.toArray(new String[installs.size()]), deflt); + } + + protected Shell getShell() { + return getControl().getShell(); + } + + protected void setNGROKs(String[] newNgroks, String dflt) { + ngroks.clear(); + for (String ngrok : newNgroks) { + ngroks.add(ngrok); + } + ngrokList.setInput(ngroks); + ngrokList.refresh(); + + for (String ngrok : newNgroks) { + if (ngrok.equals(dflt)) { + setCheckedNGROK(ngrok); + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKInstallManager.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKInstallManager.java new file mode 100644 index 000000000..60c1b3cf6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKInstallManager.java @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.ngrok; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.osgi.service.prefs.BackingStoreException; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springsource.ide.eclipse.commons.frameworks.core.util.ArrayEncoder; + +/** + * Manages the ngrok installations that are configured in this workspace. + * + * @author Martin Lippert + * @author Kris De Volder + */ +public class NGROKInstallManager { + + private static final String NGROK_INSTALLS = "ngroks"; //Key used to store info in the prefs. + private static final String DEFAULT_NGROK_INSTALL = "default.ngrok"; + + private static NGROKInstallManager instance; + + public static NGROKInstallManager getInstance() { + if (instance==null) { + instance = new NGROKInstallManager(); + } + return instance; + } + + private List installs = null; + private String defaultInstall; + + private NGROKInstallManager() { + installs = new CopyOnWriteArrayList(); + read(); + } + + private void read() { + IEclipsePreferences prefs = getPrefs(); + String defaultName = prefs.get(DEFAULT_NGROK_INSTALL, ""); + String encoded = prefs.get(NGROK_INSTALLS, null); + + if (encoded!=null) { + String[] encodedInstalls = ArrayEncoder.decode(encoded); + for (String encodedInstall : encodedInstalls) { + try { + String[] parts = ArrayEncoder.decode(encodedInstall); + String install = parts[0]; + installs.add(install); + if (defaultName.equals(install)) { + setDefaultInstall(install); + } + } catch (Exception e) { + BootActivator.log(e); + } + } + } + } + + protected IEclipsePreferences getPrefs() { + return InstanceScope.INSTANCE.getNode(BootDashActivator.PLUGIN_ID); + } + + public void save() { + List local = installs; + String[] encodedInstalls = new String[local.size()]; + for (int i = 0; i < encodedInstalls.length; i++) { + encodedInstalls[i] = encodeInstall(local.get(i)); + } + + String encoded = ArrayEncoder.encode(encodedInstalls); + IEclipsePreferences prefs = getPrefs(); + prefs.put(NGROK_INSTALLS, encoded); + + String di = null; + try { + di = getDefaultInstall(); + } catch (Exception e) { + BootActivator.log(e); + } + + if (di==null) { + prefs.remove(DEFAULT_NGROK_INSTALL); + } else { + prefs.put(DEFAULT_NGROK_INSTALL, di); + } + try { + prefs.flush(); + } catch (BackingStoreException e) { + BootActivator.log(e); + } + } + + private String encodeInstall(String install) { + return ArrayEncoder.encode(new String[] { install}); + } + + public synchronized String getDefaultInstall() { + return defaultInstall; + } + + public void setDefaultInstall(String defaultInstall) { + if (!installs.contains(defaultInstall)) { + installs.add(defaultInstall); + } + this.defaultInstall = defaultInstall; + } + + public void setInstalls(Collection newInstalls) { + this.installs = new CopyOnWriteArrayList(newInstalls); + } + + public Collection getInstalls() { + return new ArrayList(installs); + } + + public void addInstall(String ngrokInstall) { + this.installs.add(ngrokInstall); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKInstallPreferencePage.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKInstallPreferencePage.java new file mode 100644 index 000000000..e68867975 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKInstallPreferencePage.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.ngrok; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.jface.preference.PreferencePage; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.swt.custom.BusyIndicator; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.util.SWTFactory; +import org.springsource.ide.eclipse.commons.frameworks.core.ExceptionUtil; + +/** + * @author Christian Dupuis + * @author Steffen Pingel + * @author Kris De Volder + */ +public class NGROKInstallPreferencePage extends PreferencePage implements IWorkbenchPreferencePage { + + private static final String NGROK_DIALOGSETTINGS = "org.springframework.ide.eclipse.boot.dash.ngrok.dialogsettings"; + + private NGROKInstallBlock ngrokBlock; + private NGROKInstallManager installManager; + + public NGROKInstallPreferencePage() { + super("ngrok installation"); + } + + public void init(IWorkbench workbench) { + try { + installManager = NGROKInstallManager.getInstance(); + } catch (Exception e) { + throw ExceptionUtil.unchecked(e); + } + } + + @Override + public boolean isValid() { +// if (super.isValid()) { +// if (getCurrentDefaultVM() == null && fJREBlock.getJREs().length > 0) { +// setErrorMessage("Select a default Boot installation"); +// return false; +// } +// } + return true; + } + + private String getCurrentDefaultNGROK() { + return ngrokBlock.getCheckedNGROK(); + } + + @Override + public boolean performOk() { + final boolean[] canceled = new boolean[] { false }; + BusyIndicator.showWhile(null, new Runnable() { + public void run() { + Set newInstalls = new LinkedHashSet(); + String defaultNgrok = getCurrentDefaultNGROK(); + String[] ngroks = ngrokBlock.getNGROKs(); + for (String ngrok : ngroks) { + try { + newInstalls.add(ngrok); + } catch (Exception e) { + BootActivator.log(e); + } + } + + installManager.setDefaultInstall(defaultNgrok); + installManager.setInstalls(newInstalls); + installManager.save(); + } + }); + + if (canceled[0]) { + return false; + } + + // save column widths + IDialogSettings settings = BootActivator.getDefault().getDialogSettings(); + ngrokBlock.saveColumnSettings(settings, NGROK_DIALOGSETTINGS); + + return super.performOk(); + } + + @Override + protected Control createContents(Composite ancestor) { + initializeDialogUnits(ancestor); + + noDefaultAndApplyButton(); + + GridLayout layout = new GridLayout(); + layout.numColumns = 1; + layout.marginHeight = 0; + layout.marginWidth = 0; + ancestor.setLayout(layout); + + SWTFactory + .createWrapLabel( + ancestor, + "Add, edit or remove ngrok installations. By default the checked ngrok installation will be used to expose apps", + 1, 300); + SWTFactory.createVerticalSpacer(ancestor, 1); + + ngrokBlock = new NGROKInstallBlock(installManager); + ngrokBlock.createControl(ancestor); + ngrokBlock.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + isValid(); + } + }); + + Control control = ngrokBlock.getControl(); + GridData data = new GridData(GridData.FILL_BOTH); + data.horizontalSpan = 1; + control.setLayoutData(data); + + ngrokBlock.restoreColumnSettings(BootActivator.getDefault().getDialogSettings(), + NGROK_DIALOGSETTINGS); + + ngrokBlock.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + setValid(false); + + String install = getCurrentDefaultNGROK(); + if (install == null) { + setErrorMessage("Select a default ngrok installation"); + } + else { + setErrorMessage(null); + setValid(true); + } + } + }); + applyDialogFont(ancestor); + return ancestor; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKLaunchTracker.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKLaunchTracker.java new file mode 100644 index 000000000..62fcf2e24 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKLaunchTracker.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.ngrok; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Martin Lippert + */ +public class NGROKLaunchTracker { + + private static Map tunnels = new ConcurrentHashMap(); + + public static void add(String tunnelName, NGROKClient ngrokClient, NGROKTunnel tunnel) { + tunnels.put(tunnelName, ngrokClient); + } + + public static NGROKClient get(String tunnelName) { + return tunnels.get(tunnelName); + } + + public static void remove(String tunnelName) { + tunnels.remove(tunnelName); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKProcess.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKProcess.java new file mode 100644 index 000000000..a0574212d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKProcess.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.ngrok; + +import java.io.File; +import java.io.FileWriter; + +import org.eclipse.jdt.launching.SocketUtil; + +/** + * @author Martin Lippert + */ +public class NGROKProcess { + + private final Process process; + private final String apiURL; + + public static NGROKProcess startNGROK(String path, String proto, String addr) throws Exception { + FileWriter fileWriter = null; + + try { + int freePort = SocketUtil.findFreePort(); + String webAddr = "localhost:" + freePort; + + File ngrokConfigFile = File.createTempFile("ngrok-config", "yml"); + ngrokConfigFile.deleteOnExit(); + + fileWriter = new FileWriter(ngrokConfigFile); + fileWriter.write("web_addr: " + webAddr + "\n"); + fileWriter.close(); + + String configFileArg = "-config=" + ngrokConfigFile.getAbsolutePath(); + + ProcessBuilder processBuilder = new ProcessBuilder(path, proto, configFileArg, addr); + Process process = processBuilder.start(); + + return new NGROKProcess(process, "http://" + webAddr); + } + finally { + if (fileWriter != null) { + fileWriter.close(); + } + } + } + + public NGROKProcess(Process process, String apiURL) { + this.process = process; + this.apiURL = apiURL; + } + + public String getApiURL() { + return apiURL; + } + + public void terminate() { + if (this.process != null) { + this.process.destroy(); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKTunnel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKTunnel.java new file mode 100644 index 000000000..5987fb9f1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/ngrok/NGROKTunnel.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.ngrok; + +/** + * @author Martin Lippert + */ +public class NGROKTunnel { + + private String name; + private String proto; + private String public_url; + private String addr; + + public NGROKTunnel(String name, String proto, String public_url, String addr) { + super(); + this.name = name; + this.proto = proto; + this.public_url = public_url; + this.addr = addr; + } + + public String getName() { + return name; + } + + public String getProto() { + return proto; + } + + public String getPublic_url() { + return public_url; + } + + public String getAddr() { + return addr; + } + + @Override + public String toString() { + return "NGROKTunnel [name=" + name + ", proto=" + proto + ", public_url=" + public_url + ", addr=" + addr + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((addr == null) ? 0 : addr.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((proto == null) ? 0 : proto.hashCode()); + result = prime * result + ((public_url == null) ? 0 : public_url.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + NGROKTunnel other = (NGROKTunnel) obj; + if (addr == null) { + if (other.addr != null) + return false; + } else if (!addr.equals(other.addr)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (proto == null) { + if (other.proto != null) + return false; + } else if (!proto.equals(other.proto)) + return false; + if (public_url == null) { + if (other.public_url != null) + return false; + } else if (!public_url.equals(other.public_url)) + return false; + return true; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/remoteapps/RemoteAppsFromBootDash.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/remoteapps/RemoteAppsFromBootDash.java new file mode 100644 index 000000000..3b535059b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/remoteapps/RemoteAppsFromBootDash.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.remoteapps; + +import java.util.List; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springsource.ide.eclipse.commons.boot.ls.remoteapps.RemoteBootAppsDataHolder; +import org.springsource.ide.eclipse.commons.boot.ls.remoteapps.RemoteBootAppsDataHolder.Contributor; +import org.springsource.ide.eclipse.commons.boot.ls.remoteapps.RemoteBootAppsDataHolder.RemoteAppData; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +public class RemoteAppsFromBootDash implements Contributor { + + @Override + public ObservableSet getRemoteApps() { + List contributors = BootDashActivator.getDefault().getInjections().getBeans(Contributor.class); + return RemoteBootAppsDataHolder.union(contributors); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/AbstractPollingAppReadyStateMonitor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/AbstractPollingAppReadyStateMonitor.java new file mode 100644 index 000000000..fbfde996c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/AbstractPollingAppReadyStateMonitor.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.util; + +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.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; + +/** + * Polling ready state monitor. + * + * An instance of this class starts checking an application's lifecyle + * repeatedly with a short delay between polls. This continues until either the + * monitor object is disposed, or the application enters the 'ready' state. + *

+ * When the application reaches ready state then its 'ready' LiveExp will change + * value from false to true. Clients who wish to respond to this 'event' can + * attach a listener to the livexp. + * + * @author Kris De Volder + * @author Alex Boyko + * + */ +public abstract class AbstractPollingAppReadyStateMonitor implements ReadyStateMonitor { + + public static final long POLLING_INTERVAL = 500/*ms*/; + + private Job job; + private LiveVariable ready = new LiveVariable<>(false); + + final public void startPolling() { + initPollingJob(); + } + + private void initPollingJob() { + this.job = new Job("Ready state poller") { + protected IStatus run(IProgressMonitor monitor) { + LiveVariable r = ready; + if (r!=null) { //null means disposed. Job may be lagging behind + r.setValue(checkReady()); + if (!r.getValue()) { + this.schedule(POLLING_INTERVAL); + } else { + // don't reschedule + } + } + return Status.OK_STATUS; + } + }; + job.setSystem(true); + job.schedule(); + } + + final public LiveExpression getReady() { + return ready; + } + + public void dispose() { + if (job!=null) { + job.cancel(); + job = null; + } + ready = null; + } + + /** + * Checks whether application is up and running + * @return true if application is up and running + */ + abstract protected boolean checkReady(); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/CancelationTokens.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/CancelationTokens.java new file mode 100644 index 000000000..7b37af958 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/CancelationTokens.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2016 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.util; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IProgressMonitor; + +/** + * Manages a set of CancelationTokens. + * + * @author Kris De Volder + */ +public class CancelationTokens { + + private String DEBUG = null; + + //Note: we don't actually have to keep a set of tokens explicitly. + // The tokens use a 'id' which is incremented on each new token. + //So it is easy to cancel all existing tokens based on a their + //id simply by remembering the 'id' where the cancelation + //occurred. All ids 'older' than the current id are 'canceled'. + + /** + * An uncancelable token that can be used by operations that don't + * need cancelation support. + */ + public static final CancelationToken NULL = new CancelationToken() { + @Override + public boolean isCanceled() { + return false; + } + }; + + private final Object SYNC = CancelationTokens.this; + + private int canceledAllBefore = 0; + private int nextId = 0; + + public CancelationTokens() { + } + + public CancelationTokens(String debug) { + this.DEBUG = debug; + } + + public interface CancelationToken { + boolean isCanceled(); + } + + public synchronized CancelationToken create() { + CancelationToken token = new ManagedToken(); + debug("creating cancelation token: "+token); + return token; + } + + private class ManagedToken implements CancelationToken { + private int id; + + private ManagedToken() { + synchronized (SYNC) { + this.id = nextId++; + } + } + + public boolean isCanceled() { + synchronized (SYNC) { + boolean isCanceled = id < canceledAllBefore; + debug("isCanceled? ["+id+"] => "+isCanceled); + return isCanceled; + } + } + + @Override + public String toString() { + return "CancelToken("+id+")"; + } + } + + public synchronized void cancelAllBefore(CancelationToken token) { + Assert.isLegal(token instanceof ManagedToken); + canceledAllBefore = Math.max(canceledAllBefore, ((ManagedToken)token).id); + debug("CancelationTokens < "+canceledAllBefore+" are Canceled"); + } + + public void cancelAll() { + canceledAllBefore = nextId; + debug("CancelationTokens < "+canceledAllBefore+" are Canceled"); + } + + private void debug(String string) { + if (DEBUG!=null) { + System.out.println(DEBUG+": "+string); + } + } + + public static CancelationToken merge(CancelationToken cancelationToken, IProgressMonitor monitor) { + return new CancelationToken() { + @Override + public boolean isCanceled() { + return cancelationToken.isCanceled() || monitor.isCanceled(); + } + }; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/CloudCliServiceReadyStateMonitor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/CloudCliServiceReadyStateMonitor.java new file mode 100644 index 000000000..d44c8a8a5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/CloudCliServiceReadyStateMonitor.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.util; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import javax.management.AttributeNotFoundException; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanException; +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; +import javax.management.ReflectionException; +import javax.management.remote.JMXConnector; + +import org.eclipse.debug.core.ILaunch; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.util.SpringApplicationLifecycleClient; +import org.springsource.ide.eclipse.commons.core.util.ProcessUtils; + +/** + * Spring Cloud CLI 1.3.X Service ready state monitor implementation. Based on + * JMX Connection and examining bean's attribute value. + * + * @author Alex Boyko + * + */ +public class CloudCliServiceReadyStateMonitor extends AbstractPollingAppReadyStateMonitor { + + private Supplier jmxConnectionProvider; + private JMXConnector connector; + private String serviceId; + + public CloudCliServiceReadyStateMonitor(Supplier jmxConnectionProvider, String serviceId) { + super(); + this.jmxConnectionProvider = jmxConnectionProvider; + this.serviceId = serviceId; + } + + public CloudCliServiceReadyStateMonitor(ILaunch launch, String id) { + this(() -> createConnector(launch), id); + } + + private static JMXConnector createConnector(ILaunch l) { + String pid = l.getAttribute(BootLaunchConfigurationDelegate.PROCESS_ID); + if (pid == null) { + return null; + } else { + if (Long.valueOf(pid) < 0) { + throw new IllegalStateException("Invalid PID"); + } else { + return ProcessUtils.createJMXConnector(pid); + } + } + } + + @Override + public void dispose() { + if (connector != null) { + try { + connector.close(); + } catch (IOException e) { + // Ignore - process might be dead already + } + connector = null; + } + super.dispose(); + } + + @Override + protected boolean checkReady() { + try { + if (connector == null) { + try { + connector = jmxConnectionProvider.get(); + } catch (IllegalStateException e) { + // Invalid PID exception. Means that attempt to calculate PID has failed, hence fall back to no JMX connection case -> show ready state as ready + e.printStackTrace(); + return true; + } + } + if (connector != null) { + MBeanServerConnection connection = connector.getMBeanServerConnection(); + try { + Set queryNames = connection.queryNames(SpringApplicationLifecycleClient.toObjectName("launcher." + serviceId + ":type=Endpoint,name=Health"), null); + // Cloud CLI service 2.x + if (!queryNames.isEmpty()) { + if (queryNames.size() == 1) { + Object o = connection.invoke(queryNames.iterator().next(),"health", + new Object[0], + new String[0]); + if (o instanceof Map) { + return "UP".equals(((Map)o).get("status")); + } +// return dataStr.contains("status=UP"); + } else if (queryNames.size() > 1) { + throw new Exception("Too many beans matching search criteria: " + queryNames); + } + } + // Legacy Clod CLI service 1.x support + queryNames = connection.queryNames(SpringApplicationLifecycleClient.toObjectName("launcher." + serviceId + ":type=Endpoint,name=healthEndpoint,identity=*"), null); + if (queryNames.size() == 1) { + Object o = connection.invoke(queryNames.iterator().next(),"getData", + new Object[0], + new String[0]); + if (o instanceof Map) { + return "UP".equals(((Map)o).get("status")); + } +// return dataStr.contains("status=UP"); + } else if (queryNames.size() > 1) { + throw new Exception("Too many beans matching search criteria: " + queryNames); + } + } catch (AttributeNotFoundException e) { + throw new IllegalStateException( + "Unexpected: attribute 'Data' not available", e); + } catch (InstanceNotFoundException e) { + return false; // Instance not available yet + } catch (MBeanException e) { + throw new Exception(e.getCause()); + } catch (ReflectionException e) { + throw new Exception("Failed to retrieve Data attribute", + e.getCause()); + } + } + } catch (Exception e) { + if (connector != null) { + try { + connector.close(); + } catch (IOException ex) { + // Ignore - process might be dead already + } + connector = null; + } + } + return false; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/CollectionUtils.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/CollectionUtils.java new file mode 100644 index 000000000..895dd604b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/CollectionUtils.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2015 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.util; + +import java.util.Collection; + +import com.google.common.collect.ImmutableSet; + +public class CollectionUtils { + + public static T getSingle(Collection configs) { + if (configs.size()==1) { + for (T t : configs) { + return t; + } + } + return null; + } + + public static T getAny(Collection c) { + if (c!=null && !c.isEmpty()) { + for (T t : c) { + return t; + } + } + return null; + } + + public static T getAnyOr(Collection c, T orElse) { + T it = getAny(c); + return it==null?orElse:it; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/ColumnViewerAnimator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/ColumnViewerAnimator.java new file mode 100644 index 000000000..0e004a6c6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/ColumnViewerAnimator.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * Copyright (c) 2015 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.util; + +import java.util.HashMap; +import java.util.Map; + +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.viewers.ColumnViewer; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.TreeItem; +import org.eclipse.swt.widgets.Widget; +import org.eclipse.ui.progress.UIJob; + +/** + * A ColumnViewerAnimator manages a Job that periodically updates labels for + * 'animated' label icons in a ColumnViewer (actually, only TableViewer or + * TreeViewer are supported with current implementation). + * + * @author Kris De Volder + */ +public class ColumnViewerAnimator { + + /** + * Target provides abstraction needed so that we can 'setImage' easily on ViewerCell + * from either Table or Tree viewer. + */ + private static abstract class Target { + private static final Target NULL_TARGET = new Target(null) { + //Using 'widget = null' means this Target behaves like a disposed widget, which + // means that the animator will ignore / remove it and not keep a Job spinning + // to essentially do nothing animating it. + void setImage(Image image) { + } + }; + private Widget widget; + + public Target(Widget widget) { + this.widget = widget; + } + + static Target from(ViewerCell cell) { + final Widget item = cell.getItem(); + final int col = cell.getColumnIndex(); + if (item instanceof TableItem) { + return new Target(item) { + void setImage(Image image) { + ((TableItem)item).setImage(col, image); + } + }; + } else if (item instanceof TreeItem) { + return new Target(item) { + void setImage(Image image) { + ((TreeItem)item).setImage(col, image); + } + }; + } else { + return NULL_TARGET; + } + } + + abstract void setImage(Image image); + + public boolean isDisposed() { + return widget==null || widget.isDisposed(); + } + } + + public class CellAnimation { + public final Image[] imgs; + public final Target item; //could be TableItem or TreeItem + + public CellAnimation(ViewerCell cell, Image[] imgs) { + this.item = Target.from(cell); + this.imgs = imgs; + } + + } + + protected static final long INTERVAL = 100; + + private int animationCounter = 0; + + private ColumnViewer tv; + + public ColumnViewerAnimator(ColumnViewer tv) { + this.tv = tv; + } + + private Map animatedElements = new HashMap(); + + private Job job; + + public void setAnimation(ViewerCell cell, Image[] images) { + if (images==null || images.length==0) { + cell.setImage(null); + stopAnimation(cell); + } else if (images.length==1) { + stopAnimation(cell); + cell.setImage(images[0]); + } else { + cell.setImage(currentImage(images)); + startAnimation(cell, images); + } + } + + private synchronized void stopAnimation(Object e) { + animatedElements.remove(e); + } + + private synchronized void startAnimation(ViewerCell cell, Image[] imgs) { + animatedElements.put(cell, new CellAnimation(cell, imgs)); + ensureJob(); + job.schedule(); + } + + private synchronized CellAnimation[] getAnimations() { + //Copy elements to avoid CME. + return animatedElements.values().toArray(new CellAnimation[animatedElements.size()]); + } + + private void ensureJob() { + if (job==null) { + job = new UIJob("Animate table icons") { + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + if (!tv.getControl().isDisposed()) { + animationCounter++; + for (CellAnimation a : getAnimations()) { + Image[] imgs = a.imgs; + if (a.item.isDisposed()) { + //See bug: https://www.pivotaltracker.com/story/show/100608788 + stopAnimation(a); + } else { + a.item.setImage(imgs[animationCounter%imgs.length]); + } + } + if (job!=null && animatedElements.size()>0) { + job.schedule(INTERVAL); + } + } + return Status.OK_STATUS; + } + + }; + job.setSystem(true); + } + } + + private Image currentImage(Image[] images) { + return images[animationCounter%images.length]; + } + + public void dispose() { + job = null; + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/DebugUtil.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/DebugUtil.java new file mode 100644 index 000000000..b677250e6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/DebugUtil.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2016 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.util; + +import org.eclipse.core.runtime.Platform; + +public class DebugUtil { + + public static boolean isDevelopment() { + String platform = ""+Platform.getLocation(); + return platform.contains("kdvolder") || platform.contains("bamboo"); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/DummyReadyStateMonitor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/DummyReadyStateMonitor.java new file mode 100644 index 000000000..e1619c2fa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/DummyReadyStateMonitor.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.util; + +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +/** + * Dummy ReadyStateProvider that can be used when querying the life-cycle state + * via JMX (or some other means) is not available for a given process. + * + * @author Kris De Volder + */ + +public class DummyReadyStateMonitor implements ReadyStateMonitor { + + public static ReadyStateMonitor create() { + return INSTANCE; + } + + @Override + public void dispose() { + } + + @Override + public LiveExpression getReady() { + return ready; + } + + //////////////// implementation ///////////////////////////////////// + + private static final ReadyStateMonitor INSTANCE = new DummyReadyStateMonitor(); + private LiveExpression ready = LiveExpression.constant(true); + + private DummyReadyStateMonitor() { + //Stateless instance, don't create, just use the singleton instance + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/HiddenElementsLabel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/HiddenElementsLabel.java new file mode 100644 index 000000000..4b86126f4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/HiddenElementsLabel.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2015 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.util; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +public class HiddenElementsLabel implements ValueListener { + private Label label; + private LiveVariable hiddenElementCount; + + public HiddenElementsLabel(Composite page, LiveVariable hiddenElementCount) { + this.label = new Label(page, SWT.NONE); + label.setBackground(page.getBackground()); + this.hiddenElementCount = hiddenElementCount; + hiddenElementCount.addListener(this); + } + + @Override + public void gotValue(LiveExpression exp, Integer value) { + if (label.isDisposed()) { + hiddenElementCount.removeListener(this); + } else { + label.setText(value+" elements hidden by filter"); + hide(value==0); + label.getParent().layout(new Control[]{label}); + //May need this is we make element 'disapear' from layout: + // ReflowUtil.reflow(owner, this); + } + } + + private void hide(boolean shouldHide) { + if (isHide()!=shouldHide) { + GridData d = new GridData(); + d.exclude = shouldHide; + label.setLayoutData(d); + } + } + + private boolean isHide() { + if (label!=null) { + Object d = label.getLayoutData(); + if (d instanceof GridData) { + return ((GridData) d).exclude; + } + } + return false; + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LaunchConfRunStateTracker.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LaunchConfRunStateTracker.java new file mode 100644 index 000000000..ccb24ae8f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LaunchConfRunStateTracker.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2015, 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.util; + +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.springframework.ide.eclipse.boot.launch.util.BootLaunchUtils; + +/** + * @author Kris De Volder + */ +public class LaunchConfRunStateTracker extends RunStateTracker { + + @Override + protected ILaunchConfiguration getOwner(ILaunch l) { + ILaunchConfiguration conf = l.getLaunchConfiguration(); + // Check that it's a Boot App launch to filter out Spring Cloud CLI service launch from the check + if (conf instanceof ILaunchConfigurationWorkingCopy && BootLaunchUtils.isBootLaunch(l)) { + //Because ngrok expose cheats and launches a working copy... + return ((ILaunchConfigurationWorkingCopy)conf).getOriginal(); + } + return conf; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LaunchConfigurationTracker.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LaunchConfigurationTracker.java new file mode 100644 index 000000000..ed596cfc9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LaunchConfigurationTracker.java @@ -0,0 +1,193 @@ +/******************************************************************************* + * Copyright (c) 2012 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.util; + +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.isHiddenFromBootDash; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +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.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationListener; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchManager; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; + +/** + * This class is responsible of maintaining a map of {@link ILaunchConfiguration} + * that represent the children of {@link BootProjectDashElement}s. + * + * @author Kris De Volder + */ +public class LaunchConfigurationTracker implements Disposable { + + private final AtomicBoolean initialized = new AtomicBoolean(false); + private final ILaunchManager launchManager; + private final ILaunchConfigurationType launchType; + private final Map> configs = new HashMap<>(); + private ILaunchConfigurationListener launchConfListener; + private Job refreshJob; + + public LaunchConfigurationTracker(String launchTypeId, ILaunchManager launchManager) { + this.launchManager = launchManager; + this.launchType = launchManager.getLaunchConfigurationType(launchTypeId); + } + + public LaunchConfigurationTracker(String typeId) { + this(typeId, DebugPlugin.getDefault().getLaunchManager()); + } + + private void init() { + if (initialized.compareAndSet(false, true)) { + launchManager.addLaunchConfigurationListener(launchConfListener = new ILaunchConfigurationListener() { + @Override + public void launchConfigurationRemoved(ILaunchConfiguration configuration) { + //Careful, do not call 'refreshIfNeeded' here. It is impossible to determine + // the type of a deleted config (eclipse will throw an exeption if you try) + //So we have to call refresh directly here. + refresh(); + } + + @Override + public void launchConfigurationChanged(ILaunchConfiguration configuration) { + refreshIfNeeded(configuration); + } + + @Override + public void launchConfigurationAdded(ILaunchConfiguration configuration) { + refreshIfNeeded(configuration); + } + + private void refreshIfNeeded(ILaunchConfiguration configuration) { + try { + if (configuration!=null && launchType.equals(configuration.getType())) { + refresh(); + } + } catch (CoreException e) { + BootActivator.log(e); + } + } + }); + refresh(); + } + } + + private void refresh() { + refreshJob().schedule(); + } + + private synchronized Job refreshJob() { + if (refreshJob==null) { + refreshJob = new Job("Refresh Launch Conf Boot Dash Elements") { + protected IStatus run(IProgressMonitor arg0) { + Map> newSets = new HashMap<>(); + synchronized (LaunchConfigurationTracker.this) { + for (IProject oldProject : configs.keySet()) { + //enure there's at least an empty set for any relevant project + //in the newSets map: + getSet(newSets, oldProject); + } + } + for (ILaunchConfiguration conf : getRelevantConfs()) { + IProject project = BootLaunchConfigurationDelegate.getProject(conf); + if (project!=null) { + add(newSets, project, conf); + } + } + for (Entry> newEntry : newSets.entrySet()) { + IProject newProject = newEntry.getKey(); + LiveSetVariable liveset = getVar(newProject); + liveset.replaceAll(newEntry.getValue()); + } + return Status.OK_STATUS; + } + }; + refreshJob.setSystem(true); + } + return refreshJob; + } + + private void add(Map> index, IProject project, ILaunchConfiguration conf) { + getSet(index, project).add(conf); + } + + private Set getSet(Map> index, + IProject project) { + Set elements = index.get(project); + if (elements==null) { + index.put(project, elements = new HashSet<>()); + } + return elements; + } + + public ObservableSet getConfigs(IProject project) { + init(); + return getVar(project); + } + + private synchronized LiveSetVariable getVar(IProject project) { + LiveSetVariable existing = configs.get(project); + if (existing==null) { + configs.put(project, existing = new LiveSetVariable<>(AsyncMode.SYNC)); + } + return existing; + } + + private ImmutableSet getRelevantConfs() { + try { + ILaunchConfiguration[] allConfigs = launchManager.getLaunchConfigurations(launchType); + Builder builder = ImmutableSet.builder(); + for (ILaunchConfiguration c : allConfigs) { + if (isRelevant(c)) { + builder.add(c); + } + } + return builder.build(); + } catch (Exception e) { + BootActivator.log(e); + return ImmutableSet.of(); + } + } + + private boolean isRelevant(ILaunchConfiguration c) { + //Note: no need to check the launch conf type as only configs of the right type are passed in here. + return !isHiddenFromBootDash(c); + } + + @Override + public synchronized void dispose() { + if (launchConfListener!=null) { + launchManager.removeLaunchConfigurationListener(launchConfListener); + launchConfListener = null; + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LineBasedStreamGobler.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LineBasedStreamGobler.java new file mode 100644 index 000000000..45fb0215f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LineBasedStreamGobler.java @@ -0,0 +1,45 @@ +package org.springframework.ide.eclipse.boot.dash.util; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.function.Consumer; + +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public class LineBasedStreamGobler extends Thread { + + private final Consumer echo; + private BufferedReader toRead; //Stream to read. This is nulled after all input has been consumed. + + /** + * Creates a LineBasedStreamGobler that reads input from an input stream + * and writes it out to an outputstream. + */ + public LineBasedStreamGobler(InputStream toRead, Consumer lineConsumer) { + this.toRead = new BufferedReader(new InputStreamReader(toRead)); + this.echo = lineConsumer; + start(); + } + + @Override + public void run() { + while (toRead!=null) { + try { + String line = toRead.readLine(); + if (line==null) { + toRead = null; + } else { + echo.accept(line); + } + } catch (IOException e) { + toRead = null; + Log.log(e); + } + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LogSink.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LogSink.java new file mode 100644 index 000000000..cce4152fe --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/LogSink.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2015 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.util; + +/** + * An instance reprsents some place where one can send log messages. + * + * @author Kris De Volder + */ +public interface LogSink { + void log(String msg); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/MenuUtil.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/MenuUtil.java new file mode 100644 index 000000000..239500d41 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/MenuUtil.java @@ -0,0 +1,133 @@ +/******************************************************************************* + * Copyright (c) 2019, 2020 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.util; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.function.Supplier; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuCreator; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.springframework.ide.eclipse.boot.dash.views.sections.DynamicSubMenuSupplier; +import org.springsource.ide.eclipse.commons.livexp.core.UIValueListener; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public class MenuUtil { + + /** + * Adds a submenu containing a dynamically computed list of actions. The list of actions + * is computed on demand when the submenu is about to show. + */ + public static void addDynamicSubmenu(IMenuManager parent, DynamicSubMenuSupplier lazyActions) { + if (lazyActions!=null && lazyActions.isVisible()) { + MenuManager submenu = new MenuManager(lazyActions.getLabel()) { + public boolean isEnabled() { + return lazyActions.isEnabled().getValue(); + } + + @Override + public void fill(Menu parent, int index) { + super.fill(parent, index); + try { + Field f = MenuManager.class.getDeclaredField("menuItem"); + f.setAccessible(true); + MenuItem mi = (MenuItem) f.get(this); + if (mi!=null) { + Disposable enablementUpdater = lazyActions.isEnabled().onChange(UIValueListener.from((e, v) -> { + if (!mi.isDisposed()) { + mi.setEnabled(e.getValue()); + } + })); + mi.addDisposeListener(evt -> enablementUpdater.dispose()); + } + } catch (Exception e) { + Log.log(e); + } + } + }; + submenu.setRemoveAllWhenShown(true); + submenu.addMenuListener(menuAboutToShow -> { + List actions = lazyActions.getActions(); + for (IAction a : actions) { + menuAboutToShow.add(a); + } + }); + submenu.setImageDescriptor(lazyActions.getImageDescriptor()); + parent.add(submenu); + } + } + + public static void addDynamicSubmenu(IToolBarManager toolbar, DynamicSubMenuSupplier lazyActions) { + if (lazyActions!=null) { + String label = lazyActions.getLabel(); + ImageDescriptor imageDescriptor = lazyActions.getImageDescriptor(); + ImageDescriptor imageDescriptorDisabled = lazyActions.getDisabledImageDescriptor(); + Action dropdownAction=new Action(label, SWT.DROP_DOWN){}; + dropdownAction.setImageDescriptor(imageDescriptor); + dropdownAction.setDisabledImageDescriptor(imageDescriptorDisabled); + dropdownAction.setMenuCreator(new IMenuCreator() { + Menu theMenu; + + @Override + public Menu getMenu(Menu parent) { + return null; + } + + @Override + public Menu getMenu(Control parent) { + if (theMenu==null) { + final MenuManager menu = createDynamicPulldownMenuManager(label, imageDescriptor, () -> lazyActions.getActions()); + theMenu = menu.createContextMenu(parent); + theMenu.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + menu.dispose(); + } + }); + } + return theMenu; + } + + @Override + public void dispose() { + } + }); + + lazyActions.isEnabled().onChange(UIValueListener.from((e, v) -> { + dropdownAction.setEnabled(e.getValue()); + })); + + toolbar.add(new ToolbarPulldownContributionItem(dropdownAction)); + } + } + + private static MenuManager createDynamicPulldownMenuManager(String label, ImageDescriptor imageDescriptor, Supplier> actionSupplier) { + final MenuManager menu = new MenuManager(label, imageDescriptor, null); + menu.setRemoveAllWhenShown(true); + menu.addMenuListener((IMenuManager manager) -> { + for (IAction a : actionSupplier.get()) { + menu.add(a); + } + }); + return menu; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/NameableLabelProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/NameableLabelProvider.java new file mode 100644 index 000000000..f045430ff --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/NameableLabelProvider.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2015 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.util; + +import org.springframework.ide.eclipse.boot.dash.model.Nameable; +import org.springsource.ide.eclipse.commons.livexp.ui.SimpleLabelProvider; + +/** + * Label provider that simly displays the 'name' of any elements that + * implement {@link Nameable}. + * + * @author Kris De Volder + */ +public final class NameableLabelProvider extends SimpleLabelProvider { + public String getText(Object element) { + if (element instanceof Nameable) { + return ((Nameable) element).getName(); + } + return super.getText(element); + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/OrderBasedComparator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/OrderBasedComparator.java new file mode 100644 index 000000000..e98f81d2f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/OrderBasedComparator.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2015 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.util; + +import java.util.Comparator; + +/** + * Comparator that knows how to compare finite number of objects, based on + * a desired sorting order that is provided to it in the constructor. + *

+ * Asking this comparator to compare other objects than those provided + * in the constructor will raise an IllegalArgumentException. + * + * @author Kris De Volder + */ +public class OrderBasedComparator implements Comparator { + + private T[] sortedElements; + + public OrderBasedComparator(T... sortedElements) { + this.sortedElements = sortedElements; + } + + @Override + public int compare(T t1, T t2) { + return getIndex(t1) - getIndex(t2); + } + + private int getIndex(T t) { + for (int i = 0; i < sortedElements.length; i++) { + if (sortedElements[i].equals(t)) { + return i; + } + } + throw new IllegalArgumentException("This comparator doesn't know how to compare with "+t); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/Performable.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/Performable.java new file mode 100644 index 000000000..f723e4b53 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/Performable.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.util; + +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +/** + * Like {@link Runnable} called in a 'ui' context, it also + * allows throwing Exceptions (for the convenience of implementors). + *

+ * It is the responsibility of callers to deal with the exceptions + * (e.g. by logging problems or showing an error popup). + */ +public interface Performable { + void perform(UserInteractions ui) throws Exception; +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/ReadyStateMonitor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/ReadyStateMonitor.java new file mode 100644 index 000000000..813c48269 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/ReadyStateMonitor.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.util; + +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +/** + * Represents an entity that actively monitors the 'ready' state of some + * other entity (e.g. a running boot application). The ReadyStateMonitor exposes + * a 'ready' state variable which it sets to 'true' when it observes that the + * monitored process has booted-up succesfully. + * + * @author Kris De Volder + */ +public interface ReadyStateMonitor { + + /** + * The initial state of this will always be false. If/when the monitored enitty + * becomes ready this changes to true. Once this happens the ReadyStateMonitor + * stops watching the monitored enitity and the statevariable will no longer + * be updated (not even when the process terminates). + */ + LiveExpression getReady(); + + /** + * Cleanup whatever resources are being used by the monitoring enitity. + * (e.g. jobs that it schedules periodically to poll and update the 'ready' state.) + */ + void dispose(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/RunStateTracker.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/RunStateTracker.java new file mode 100644 index 000000000..f86196b14 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/RunStateTracker.java @@ -0,0 +1,275 @@ +/******************************************************************************* + * Copyright (c) 2015, 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.util; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.eclipse.core.runtime.ListenerList; +import org.eclipse.core.runtime.Platform; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.model.IProcess; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.cli.CloudCliServiceLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.util.BootLaunchUtils; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springframework.ide.eclipse.boot.util.ProcessListenerAdapter; +import org.springframework.ide.eclipse.boot.util.ProcessTracker; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +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; + +/** + * Generalization of OwnerRunStateTracker. An instance of this class tracks active processes + * in the eclipse DebugUI and maitains a 'RunState' that is associated indirectly with some + * object these processes belong to. + *

+ * This is an abstract class as clients need to implement the 'getOwner' method to define + * what enitity a given launch configuration belongs to. + * + * @author Kris De Volder + */ +public abstract class RunStateTracker extends ProcessListenerAdapter implements Disposable { + + //// public API /////////////////////////////////////////////////////////////////// + + public interface RunStateListener { + void stateChanged(T owner); + } + + public RunStateTracker() { + activeStates = new HashMap<>(); + processTracker = new ProcessTracker(this); + updateOwnerStatesAndFireEvents(); + } + + public synchronized RunState getState(final T owner) { + return getState(activeStates, owner); + } + + public void addListener(RunStateListener listener) { + this.listeners.add(listener); + } + + public void removeListener(RunStateListener listener) { + this.listeners.remove(listener); + } + + ///////////////////////// stuff below is implementation cruft //////////////////// + + private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder"); + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + private Map activeStates = null; + private Map readyStateTrackers = null; + private ProcessTracker processTracker = null; + private ListenerList listeners = new ListenerList(); // listeners that are interested in us (i.e. clients) + + private static RunState getState(Map states, T p) { + if (states!=null) { + RunState state = states.get(p); + if (state!=null) { + return state; + } + } + return RunState.INACTIVE; + } + + private Map getCurrentActiveStates() { + Map states = new HashMap<>(); + for (ILaunch l : launchManager().getLaunches()) { + if (!l.isTerminated() && isInteresting(l)) { + T p = getOwner(l); + RunState s1 = getState(states, p); + RunState s2 = getActiveState(l); + states.put(p, s1.merge(s2)); + } + } + return states; + } + + protected abstract T getOwner(ILaunch l); + + protected boolean isInteresting(ILaunch l) { + return BootLaunchUtils.isBootLaunch(l) + || CloudCliServiceLaunchConfigurationDelegate.isLocalCloudServiceLaunch(l.getLaunchConfiguration()); + } + + /** + * Assuming that l is an active launch, determine its RunState. + */ + private RunState getActiveState(ILaunch l) { + boolean isReady = getReadyState(l).getValue(); + if (isReady) { + return BootLaunchUtils.isDebugging(l) + ? RunState.DEBUGGING + : RunState.RUNNING; + } + return RunState.STARTING; + } + + private synchronized LiveExpression getReadyState(ILaunch l) { + if (readyStateTrackers==null) { + readyStateTrackers = new HashMap<>(); + } + ReadyStateMonitor tracker = readyStateTrackers.get(l); + if (tracker==null) { + readyStateTrackers.put(l, tracker = createReadyStateTracker(l)); + tracker.getReady().addListener(readyStateListener); +// } else { +// debug("getReadyState["+l+"] "+BootLaunchUtils.getProject(l)+" FROM CACHE"); + } + return tracker.getReady(); + } + + private ValueListener readyStateListener = new ValueListener() { + public void gotValue(LiveExpression exp, Boolean value) { + if (value) { + //ready state tracker detected a launch just entered the 'ready' state + updateOwnerStatesAndFireEvents(); + } + } + }; + private boolean updateInProgress; + + protected final ReadyStateMonitor createReadyStateTracker(ILaunch l) { + try { + Boolean canUseLifeCycle=null, cliCanUseLifeCycle=null, isSingleProcessServiceLaunch = null; + if ((canUseLifeCycle = BootLaunchConfigurationDelegate.canUseLifeCycle(l)) || (cliCanUseLifeCycle = CloudCliServiceLaunchConfigurationDelegate.canUseLifeCycle(l))) { + SpringApplicationReadyStateMonitor readyStateMonitor = new SpringApplicationReadyStateMonitor(l); + readyStateMonitor.startPolling(); + debug("createReadyStateTracker"+"["+l+"] "+BootLaunchUtils.getProject(l)+" OK!"); + return readyStateMonitor; + } else if (isSingleProcessServiceLaunch = CloudCliServiceLaunchConfigurationDelegate.isSingleProcessServiceConfig(l.getLaunchConfiguration())) { + String serviceId = l.getLaunchConfiguration().getAttribute(CloudCliServiceLaunchConfigurationDelegate.ATTR_CLOUD_SERVICE_ID, (String) null); + if (serviceId != null) { + CloudCliServiceReadyStateMonitor readyStateMonitor = new CloudCliServiceReadyStateMonitor(l, serviceId); + readyStateMonitor.startPolling(); + return readyStateMonitor; + } + } + debug("createReadyStateTracker"+"["+l+"] "+BootLaunchUtils.getProject(l)+" NOT APPLICABLE"); + debug(" canUseLifeCycle="+canUseLifeCycle+"\n cliCanUseLifeCycle="+cliCanUseLifeCycle+"\n isSingleProcessServiceLaunch="+isSingleProcessServiceLaunch); + } catch (Exception e) { + debug("createReadyStateTracker"+"["+l+"] "+BootLaunchUtils.getProject(l)+" FAILED: "+ExceptionUtil.getMessage(e)); + Log.log(e); + } + return DummyReadyStateMonitor.create(); + } + + private synchronized void cleanupReadyStateTrackers() { + if (readyStateTrackers!=null) { + Iterator> iter = readyStateTrackers.entrySet().iterator(); + while(iter.hasNext()) { + Entry entry = iter.next(); + ILaunch l = entry.getKey(); + if (l.isTerminated()) { + ReadyStateMonitor tracker = entry.getValue(); + iter.remove(); + tracker.dispose(); + } + } + } + } + + protected ILaunchManager launchManager() { + return DebugPlugin.getDefault().getLaunchManager(); + } + + public void dispose() { + if (processTracker!=null) { + processTracker.dispose(); + processTracker = null; + } + } + + private void updateOwnerStatesAndFireEvents() { + //Note that updateOwnerStates is synchronized, but this method is not. + // Important not to keep locks while firing events. + Set affected = updateOwnerStates(); + for (Object _l : listeners.getListeners()) { + @SuppressWarnings("unchecked") + RunStateListener listener = (RunStateListener) _l; + for (T p : affected) { + listener.stateChanged(p); + } + } + } + + private synchronized Set updateOwnerStates() { + if (updateInProgress) { + //Avoid bug caused by reentrance from same thread. + //This bug causes double update events for INAVTIVE -> RUNNING for owners that + // don't have ready state tracking and so immediately enter the ready state upon + // creation. + return Collections.emptySet(); + } else { + updateInProgress = true; + try { + Map oldStates = activeStates; + activeStates = getCurrentActiveStates(); + + // Compute set of owners who's state has changed + Set affectedOwners = new HashSet<>(keySet(oldStates)); + affectedOwners.addAll(keySet(activeStates)); + Iterator iter = affectedOwners.iterator(); + while (iter.hasNext()) { + T p = iter.next(); + RunState oldState = getState(oldStates, p); + RunState newState = getState(activeStates, p); + if (oldState.equals(newState)) { + iter.remove(); + } else { + debug(p+": "+ oldState +" => " + newState); + } + } + return affectedOwners; + } finally { + updateInProgress = false; + } + } + } + + /** + * Null-safe 'keySet' fetcher for map. + */ + private Set keySet(Map map) { + if (map==null) { + return Collections.emptySet(); + } + return map.keySet(); + } + + @Override + public void processTerminated(ProcessTracker tracker, IProcess process) { + updateOwnerStatesAndFireEvents(); + cleanupReadyStateTrackers(); + } + + @Override + public void processCreated(ProcessTracker tracker, IProcess process) { + updateOwnerStatesAndFireEvents(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/RunnableWithException.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/RunnableWithException.java new file mode 100644 index 000000000..e26d6f1e4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/RunnableWithException.java @@ -0,0 +1,5 @@ +package org.springframework.ide.eclipse.boot.dash.util; + +public interface RunnableWithException { + void run() throws Exception; +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/SpringApplicationReadyStateMonitor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/SpringApplicationReadyStateMonitor.java new file mode 100644 index 000000000..7fbcea861 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/SpringApplicationReadyStateMonitor.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2015, 2017 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.util; + +import org.eclipse.debug.core.ILaunch; +import org.springframework.ide.eclipse.boot.launch.util.SpringApplicationLifeCycleClientManager; +import org.springframework.ide.eclipse.boot.launch.util.SpringApplicationLifecycleClient; + +/** + * An instance of this class starts checking a spring application's lifecyle using + * a JMX bean protocol. Checks are performed repeatedly with a short delay between + * polls. This continues until either the SpringApplicationReadyStateMonitor is disposed, + * or the application enters the 'ready' state. + *

+ * When the application reaches ready state then its 'ready' LiveExp will change value from + * false to true. Clients who wish to respond to this 'event' can attach a listener to + * the livexp. + * + * @author Kris De Volder + */ +public class SpringApplicationReadyStateMonitor extends AbstractPollingAppReadyStateMonitor { + + private SpringApplicationLifeCycleClientManager clientManager; + + public SpringApplicationReadyStateMonitor(ILaunch launch) { + super(); + clientManager = new SpringApplicationLifeCycleClientManager(launch); + } + + public void dispose() { + if (clientManager != null) { + clientManager.disposeClient(); + } + super.dispose(); + } + + protected boolean checkReady() { + try { + SpringApplicationLifecycleClient client = clientManager.getLifeCycleClient(); + if (client!=null) { + return client.isReady(); + } + } catch (Exception e) { + //Something went wrong asking client for ready state. + // most likely process died. + if (clientManager != null) { + clientManager.disposeClient(); + } + } + return false; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/ToolbarPulldownContributionItem.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/ToolbarPulldownContributionItem.java new file mode 100644 index 000000000..dc43f98a4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/ToolbarPulldownContributionItem.java @@ -0,0 +1,1405 @@ +/******************************************************************************* + * Copyright (c) 2000, 2015 IBM Corporation and others. + * 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: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +//This file is a copy of jface's ActionContributionItem. It has been changed/hacked slighly +//to make a button / pull down toolbar item open the pulldown menu even when user +//clicks on the button icon. +//Changes are marked with comment HACK CHANGE + +package org.springframework.ide.eclipse.boot.dash.util; + +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.core.commands.NotEnabledException; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.ContributionItem; +import org.eclipse.jface.action.ExternalActionManager; +import org.eclipse.jface.action.ExternalActionManager.IBindingManagerCallback; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IContributionManagerOverrides; +import org.eclipse.jface.action.IMenuCreator; +import org.eclipse.jface.action.LegacyActionTools; +import org.eclipse.jface.bindings.Trigger; +import org.eclipse.jface.bindings.TriggerSequence; +import org.eclipse.jface.bindings.keys.IKeyLookup; +import org.eclipse.jface.bindings.keys.KeyLookupFactory; +import org.eclipse.jface.bindings.keys.KeyStroke; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.resource.LocalResourceManager; +import org.eclipse.jface.resource.ResourceManager; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.Policy; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.jface.util.Util; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Item; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.swt.widgets.Widget; + +/** + * A contribution item which delegates to an action. + *

+ * This class may be instantiated; it is not intended to be subclassed. + *

+ * @noextend This class is not intended to be subclassed by clients. + */ +public class ToolbarPulldownContributionItem extends ContributionItem { + + + /** + * Mode bit: Show text on tool items or buttons, even if an image is + * present. If this mode bit is not set, text is only shown on tool items if + * there is no image present. + * + * @since 3.0 + */ + public static int MODE_FORCE_TEXT = 1; + + /** a string inserted in the middle of text that has been shortened */ + private static final String ellipsis = "..."; //$NON-NLS-1$ + + /** + * Stores the result of the action. False when the action returned failure. + */ + private Boolean result = null; + + private static boolean USE_COLOR_ICONS = true; + + /** + * Returns whether color icons should be used in toolbars. + * + * @return true if color icons should be used in toolbars, + * false otherwise + */ + public static boolean getUseColorIconsInToolbars() { + return USE_COLOR_ICONS; + } + + /** + * Sets whether color icons should be used in toolbars. + * + * @param useColorIcons + * true if color icons should be used in toolbars, + * false otherwise + */ + public static void setUseColorIconsInToolbars(boolean useColorIcons) { + USE_COLOR_ICONS = useColorIcons; + } + + /** + * The presentation mode. + */ + private int mode = 0; + + /** + * The action. + */ + private IAction action; + + /** + * The listener for changes to the text of the action contributed by an + * external source. + */ + private final IPropertyChangeListener actionTextListener = new IPropertyChangeListener() { + + /** + * @see IPropertyChangeListener#propertyChange(PropertyChangeEvent) + */ + @Override + public void propertyChange(PropertyChangeEvent event) { + update(event.getProperty()); + } + }; + + /** + * Remembers all images in use by this contribution item + */ + private LocalResourceManager imageManager; + + /** + * Listener for SWT button widget events. + */ + private Listener buttonListener; + + /** + * Listener for SWT menu item widget events. + */ + private Listener menuItemListener; + + /** + * Listener for action property change notifications. + */ + private final IPropertyChangeListener propertyListener = new IPropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + actionPropertyChange(event); + } + }; + + /** + * Listener for SWT tool item widget events. + */ + private Listener toolItemListener; + + /** + * The widget created for this item; null before creation and + * after disposal. + */ + private Widget widget = null; + + private Listener menuCreatorListener; + + /** + * Creates a new contribution item from the given action. The id of the + * action is used as the id of the item. + * + * @param action + * the action + */ + public ToolbarPulldownContributionItem(IAction action) { + super(action.getId()); + this.action = action; + } + + /** + * Handles a property change event on the action (forwarded by nested + * listener). + */ + private void actionPropertyChange(final PropertyChangeEvent e) { + // This code should be removed. Avoid using free asyncExec + + if (isVisible() && widget != null) { + Display display = widget.getDisplay(); + if (display.getThread() == Thread.currentThread()) { + update(e.getProperty()); + } else { + display.asyncExec(new Runnable() { + @Override + public void run() { + update(e.getProperty()); + } + }); + } + + } + } + + /** + * Compares this action contribution item with another object. Two action + * contribution items are equal if they refer to the identical Action. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof ToolbarPulldownContributionItem)) { + return false; + } + return action.equals(((ToolbarPulldownContributionItem) o).action); + } + + /** + * The ActionContributionItem implementation of this + * IContributionItem method creates an SWT + * Button for the action using the action's style. If the + * action's checked property has been set, the button is created and primed + * to the value of the checked property. + */ + @Override + public void fill(Composite parent) { + if (widget == null && parent != null) { + int flags = SWT.PUSH; + if (action != null) { + if (action.getStyle() == IAction.AS_CHECK_BOX) { + flags = SWT.TOGGLE; + } + if (action.getStyle() == IAction.AS_RADIO_BUTTON) { + flags = SWT.RADIO; + } + } + + Button b = new Button(parent, flags); + b.setData(this); + b.addListener(SWT.Dispose, getButtonListener()); + // Don't hook a dispose listener on the parent + b.addListener(SWT.Selection, getButtonListener()); + if (action.getHelpListener() != null) { + b.addHelpListener(action.getHelpListener()); + } + widget = b; + + update(null); + + // Attach some extra listeners. + action.addPropertyChangeListener(propertyListener); + if (action != null) { + String commandId = action.getActionDefinitionId(); + ExternalActionManager.ICallback callback = ExternalActionManager + .getInstance().getCallback(); + + if ((callback != null) && (commandId != null)) { + callback.addPropertyChangeListener(commandId, + actionTextListener); + } + } + } + } + + /** + * The ActionContributionItem implementation of this + * IContributionItem method creates an SWT + * MenuItem for the action using the action's style. If the + * action's checked property has been set, a button is created and primed to + * the value of the checked property. If the action's menu creator property + * has been set, a cascading submenu is created. + */ + @Override + public void fill(Menu parent, int index) { + if (widget == null && parent != null) { + int flags = SWT.PUSH; + if (action != null) { + int style = action.getStyle(); + if (style == IAction.AS_CHECK_BOX) { + flags = SWT.CHECK; + } else if (style == IAction.AS_RADIO_BUTTON) { + flags = SWT.RADIO; + } else if (style == IAction.AS_DROP_DOWN_MENU) { + flags = SWT.CASCADE; + } + } + + MenuItem mi = null; + if (index >= 0) { + mi = new MenuItem(parent, flags, index); + } else { + mi = new MenuItem(parent, flags); + } + widget = mi; + + mi.setData(this); + mi.addListener(SWT.Dispose, getMenuItemListener()); + mi.addListener(SWT.Selection, getMenuItemListener()); + if (action.getHelpListener() != null) { + mi.addHelpListener(action.getHelpListener()); + } + + if (flags == SWT.CASCADE) { + // just create a proxy for now, if the user shows it then + // fill it in + Menu subMenu = new Menu(parent); + subMenu.addListener(SWT.Show, getMenuCreatorListener()); + subMenu.addListener(SWT.Hide, getMenuCreatorListener()); + mi.setMenu(subMenu); + } + + update(null); + + // Attach some extra listeners. + action.addPropertyChangeListener(propertyListener); + if (action != null) { + String commandId = action.getActionDefinitionId(); + ExternalActionManager.ICallback callback = ExternalActionManager + .getInstance().getCallback(); + + if ((callback != null) && (commandId != null)) { + callback.addPropertyChangeListener(commandId, + actionTextListener); + } + } + } + } + + /** + * The ActionContributionItem implementation of this , + * IContributionItem method creates an SWT + * ToolItem for the action using the action's style. If the + * action's checked property has been set, a button is created and primed to + * the value of the checked property. If the action's menu creator property + * has been set, a drop-down tool item is created. + */ + @Override + public void fill(ToolBar parent, int index) { + if (widget == null && parent != null) { + int flags = SWT.PUSH; + if (action != null) { + int style = action.getStyle(); + if (style == IAction.AS_CHECK_BOX) { + flags = SWT.CHECK; + } else if (style == IAction.AS_RADIO_BUTTON) { + flags = SWT.RADIO; + } else if (style == IAction.AS_DROP_DOWN_MENU) { + flags = SWT.DROP_DOWN; + } + } + + ToolItem ti = null; + if (index >= 0) { + ti = new ToolItem(parent, flags, index); + } else { + ti = new ToolItem(parent, flags); + } + ti.setData(this); + ti.addListener(SWT.Selection, getToolItemListener()); + ti.addListener(SWT.Dispose, getToolItemListener()); + + widget = ti; + + update(null); + + // Attach some extra listeners. + action.addPropertyChangeListener(propertyListener); + if (action != null) { + String commandId = action.getActionDefinitionId(); + ExternalActionManager.ICallback callback = ExternalActionManager + .getInstance().getCallback(); + + if ((callback != null) && (commandId != null)) { + callback.addPropertyChangeListener(commandId, + actionTextListener); + } + } + } + } + + /** + * Returns the action associated with this contribution item. + * + * @return the action + */ + public IAction getAction() { + return action; + } + + /** + * Returns the listener for SWT button widget events. + * + * @return a listener for button events + */ + private Listener getButtonListener() { + if (buttonListener == null) { + buttonListener = new Listener() { + @Override + public void handleEvent(Event event) { + switch (event.type) { + case SWT.Dispose: + handleWidgetDispose(event); + break; + case SWT.Selection: + Widget ew = event.widget; + if (ew != null) { + handleWidgetSelection(event, ((Button) ew) + .getSelection()); + } + break; + } + } + }; + } + return buttonListener; + } + + /** + * Returns the listener for SWT menu item widget events. + * + * @return a listener for menu item events + */ + private Listener getMenuItemListener() { + if (menuItemListener == null) { + menuItemListener = new Listener() { + @Override + public void handleEvent(Event event) { + switch (event.type) { + case SWT.Dispose: + handleWidgetDispose(event); + break; + case SWT.Selection: + Widget ew = event.widget; + if (ew != null) { + handleWidgetSelection(event, ((MenuItem) ew) + .getSelection()); + } + break; + } + } + }; + } + return menuItemListener; + } + + /** + * Returns the presentation mode, which is the bitwise-or of the + * MODE_* constants. The default mode setting is 0, meaning + * that for menu items, both text and image are shown (if present), but for + * tool items, the text is shown only if there is no image. + * + * @return the presentation mode settings + * + * @since 3.0 + */ + public int getMode() { + return mode; + } + + /** + * Returns the listener for SWT tool item widget events. + * + * @return a listener for tool item events + */ + private Listener getToolItemListener() { + if (toolItemListener == null) { + toolItemListener = new Listener() { + @Override + public void handleEvent(Event event) { + switch (event.type) { + case SWT.Dispose: + handleWidgetDispose(event); + break; + case SWT.Selection: + Widget ew = event.widget; + if (ew != null) { + handleWidgetSelection(event, ((ToolItem) ew) + .getSelection()); + } + break; + } + } + }; + } + return toolItemListener; + } + + /** + * Handles a widget dispose event for the widget corresponding to this item. + */ + private void handleWidgetDispose(Event e) { + // Check if our widget is the one being disposed. + if (e.widget == widget) { + // Dispose of the menu creator. + if (action.getStyle() == IAction.AS_DROP_DOWN_MENU + && menuCreatorCalled) { + IMenuCreator mc = action.getMenuCreator(); + if (mc != null) { + mc.dispose(); + } + } + + // Unhook all of the listeners. + action.removePropertyChangeListener(propertyListener); + if (action != null) { + String commandId = action.getActionDefinitionId(); + ExternalActionManager.ICallback callback = ExternalActionManager + .getInstance().getCallback(); + + if ((callback != null) && (commandId != null)) { + callback.removePropertyChangeListener(commandId, + actionTextListener); + } + } + + // Clear the widget field. + widget = null; + + disposeOldImages(); + } + } + + /** + * Handles a widget selection event. + */ + private void handleWidgetSelection(Event e, boolean selection) { + + Widget item = e.widget; + if (item != null) { + int style = item.getStyle(); + + if ((style & (SWT.TOGGLE | SWT.CHECK)) != 0) { + if (action.getStyle() == IAction.AS_CHECK_BOX) { + action.setChecked(selection); + } + } else if ((style & SWT.RADIO) != 0) { + if (action.getStyle() == IAction.AS_RADIO_BUTTON) { + action.setChecked(selection); + } + } else if ((style & SWT.DROP_DOWN) != 0) { + //HACK CHANGE start + //if (e.detail == 4) { // on drop-down button + //HACK CHANGE end + if (action.getStyle() == IAction.AS_DROP_DOWN_MENU) { + IMenuCreator mc = action.getMenuCreator(); + menuCreatorCalled = true; + ToolItem ti = (ToolItem) item; + // we create the menu as a sub-menu of "dummy" so that + // we can use + // it in a cascading menu too. + // If created on a SWT control we would get an SWT + // error... + // Menu dummy= new Menu(ti.getParent()); + // Menu m= mc.getMenu(dummy); + // dummy.dispose(); + if (mc != null) { + Menu m = mc.getMenu(ti.getParent()); + if (m != null) { + // position the menu below the drop down item + //Point point = ti.getParent().toDisplay( + // new Point(e.x, e.y)); + //HACK CHANGE start + //When clicking the button the event position is actually 0,0 + // use something better. + Point point = new Point(e.x, e.y); + if (point.x==0 && point.y==0) { + Rectangle bnds = ti.getBounds(); + point.x = bnds.x; + point.y = bnds.y+bnds.height; + } + point = ti.getParent().toDisplay(point); + //HACK CHANGE end + m.setLocation(point.x, point.y); // waiting + // for SWT + // 0.42 + m.setVisible(true); + return; // we don't fire the action + } + } + } + //HACK CHANGE: } + } + + ExternalActionManager.IExecuteCallback callback = null; + String actionDefinitionId = action.getActionDefinitionId(); + if (actionDefinitionId != null) { + Object obj = ExternalActionManager.getInstance() + .getCallback(); + if (obj instanceof ExternalActionManager.IExecuteCallback) { + callback = (ExternalActionManager.IExecuteCallback) obj; + } + } + + // Ensure action is enabled first. + // See 1GAN3M6: ITPUI:WINNT - Any IAction in the workbench can be + // executed while disabled. + if (action.isEnabled()) { + boolean trace = Policy.TRACE_ACTIONS; + + long ms = 0L; + if (trace) { + ms = System.currentTimeMillis(); + System.out.println("Running action: " + action.getText()); //$NON-NLS-1$ + } + + IPropertyChangeListener resultListener = null; + if (callback != null) { + resultListener = new IPropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + // Check on result + if (event.getProperty().equals(IAction.RESULT)) { + if (event.getNewValue() instanceof Boolean) { + result = (Boolean) event.getNewValue(); + } + } + } + }; + action.addPropertyChangeListener(resultListener); + callback.preExecute(action, e); + } + + action.runWithEvent(e); + + if (callback != null) { + if (result == null || result.equals(Boolean.TRUE)) { + callback.postExecuteSuccess(action, Boolean.TRUE); + } else { + callback.postExecuteFailure(action, + new ExecutionException(action.getText() + + " returned failure.")); //$NON-NLS-1$ + } + } + + if (resultListener!=null) { + result = null; + action.removePropertyChangeListener(resultListener); + } + if (trace) { + System.out.println((System.currentTimeMillis() - ms) + + " ms to run action: " + action.getText()); //$NON-NLS-1$ + } + } else { + if (callback != null) { + callback.notEnabled(action, new NotEnabledException(action + .getText() + + " is not enabled.")); //$NON-NLS-1$ + } + } + } + } + + @Override + public int hashCode() { + return action.hashCode(); + } + + /** + * Returns whether the given action has any images. + * + * @param actionToCheck + * the action + * @return true if the action has any images, + * false if not + */ + private boolean hasImages(IAction actionToCheck) { + return actionToCheck.getImageDescriptor() != null + || actionToCheck.getHoverImageDescriptor() != null + || actionToCheck.getDisabledImageDescriptor() != null; + } + + /** + * Returns whether the command corresponding to this action is active. + */ + private boolean isCommandActive() { + IAction actionToCheck = getAction(); + + if (actionToCheck != null) { + String commandId = actionToCheck.getActionDefinitionId(); + ExternalActionManager.ICallback callback = ExternalActionManager + .getInstance().getCallback(); + + if (callback != null) { + return callback.isActive(commandId); + } + } + return true; + } + + /** + * The action item implementation of this IContributionItem + * method returns true for menu items and false + * for everything else. + */ + @Override + public boolean isDynamic() { + if (widget instanceof MenuItem) { + // Optimization. Only recreate the item is the check or radio style + // has changed. + boolean itemIsCheck = (widget.getStyle() & SWT.CHECK) != 0; + boolean actionIsCheck = getAction() != null + && getAction().getStyle() == IAction.AS_CHECK_BOX; + boolean itemIsRadio = (widget.getStyle() & SWT.RADIO) != 0; + boolean actionIsRadio = getAction() != null + && getAction().getStyle() == IAction.AS_RADIO_BUTTON; + return (itemIsCheck != actionIsCheck) + || (itemIsRadio != actionIsRadio); + } + return false; + } + + @Override + public boolean isEnabled() { + return action != null && action.isEnabled(); + } + + /** + * Returns true if this item is allowed to enable, + * false otherwise. + * + * @return if this item is allowed to be enabled + * @since 2.0 + */ + protected boolean isEnabledAllowed() { + if (getParent() == null) { + return true; + } + Boolean value = getParent().getOverrides().getEnabled(this); + return (value == null) ? true : value.booleanValue(); + } + + /** + * The ActionContributionItem implementation of this + * ContributionItem method extends the super implementation + * by also checking whether the command corresponding to this action is + * active. + */ + @Override + public boolean isVisible() { + return super.isVisible() && isCommandActive(); + } + + /** + * Sets the presentation mode, which is the bitwise-or of the + * MODE_* constants. + * + * @param mode + * the presentation mode settings + * + * @since 3.0 + */ + public void setMode(int mode) { + this.mode = mode; + update(); + } + + /** + * The action item implementation of this IContributionItem + * method calls update(null). + */ + @Override + public final void update() { + update(null); + } + + /** + * Synchronizes the UI with the given property. + * + * @param propertyName + * the name of the property, or null meaning all + * applicable properties + */ + @Override + public void update(String propertyName) { + if (widget != null) { + // determine what to do + boolean textChanged = propertyName == null + || propertyName.equals(IAction.TEXT); + boolean imageChanged = propertyName == null + || propertyName.equals(IAction.IMAGE); + boolean tooltipTextChanged = propertyName == null + || propertyName.equals(IAction.TOOL_TIP_TEXT); + boolean enableStateChanged = propertyName == null + || propertyName.equals(IAction.ENABLED) + || propertyName + .equals(IContributionManagerOverrides.P_ENABLED); + boolean checkChanged = (action.getStyle() == IAction.AS_CHECK_BOX || action + .getStyle() == IAction.AS_RADIO_BUTTON) + && (propertyName == null || propertyName + .equals(IAction.CHECKED)); + + if (widget instanceof ToolItem) { + ToolItem ti = (ToolItem) widget; + String text = action.getText(); + // the set text is shown only if there is no image or if forced + // by MODE_FORCE_TEXT + boolean showText = text != null + && ((getMode() & MODE_FORCE_TEXT) != 0 || !hasImages(action)); + + // only do the trimming if the text will be used + if (showText && text != null) { + text = Action.removeAcceleratorText(text); + text = Action.removeMnemonics(text); + } + + if (textChanged) { + String textToSet = showText ? text : ""; //$NON-NLS-1$ + boolean rightStyle = (ti.getParent().getStyle() & SWT.RIGHT) != 0; + if (rightStyle || !ti.getText().equals(textToSet)) { + // In addition to being required to update the text if + // it + // gets nulled out in the action, this is also a + // workaround + // for bug 50151: Using SWT.RIGHT on a ToolBar leaves + // blank space + ti.setText(textToSet); + } + } + + if (imageChanged) { + // only substitute a missing image if it has no text + updateImages(!showText); + } + + if (tooltipTextChanged || textChanged) { + String toolTip = action.getToolTipText(); + if ((toolTip == null) || (toolTip.length() == 0)) { + toolTip = text; + } + + ExternalActionManager.ICallback callback = ExternalActionManager + .getInstance().getCallback(); + String commandId = action.getActionDefinitionId(); + if ((callback != null) && (commandId != null) + && (toolTip != null)) { + String acceleratorText = callback + .getAcceleratorText(commandId); + if (acceleratorText != null + && acceleratorText.length() != 0) { + toolTip = JFaceResources.format( + "Toolbar_Tooltip_Accelerator", //$NON-NLS-1$ + new Object[] { toolTip, acceleratorText }); + } + } + + // if the text is showing, then only set the tooltip if + // different + if (!showText || toolTip != null && !toolTip.equals(text)) { + ti.setToolTipText(toolTip); + } else { + ti.setToolTipText(null); + } + } + + if (enableStateChanged) { + boolean shouldBeEnabled = action.isEnabled() + && isEnabledAllowed(); + + if (ti.getEnabled() != shouldBeEnabled) { + ti.setEnabled(shouldBeEnabled); + } + } + + if (checkChanged) { + boolean bv = action.isChecked(); + + if (ti.getSelection() != bv) { + ti.setSelection(bv); + } + } + return; + } + + if (widget instanceof MenuItem) { + MenuItem mi = (MenuItem) widget; + + if (textChanged) { + int accelerator = 0; + String acceleratorText = null; + IAction updatedAction = getAction(); + String text = null; + accelerator = updatedAction.getAccelerator(); + ExternalActionManager.ICallback callback = ExternalActionManager + .getInstance().getCallback(); + + // Block accelerators that are already in use. + if ((accelerator != 0) && (callback != null) + && (callback.isAcceleratorInUse(accelerator))) { + accelerator = 0; + } + + /* + * Process accelerators on GTK in a special way to avoid Bug + * 42009. We will override the native input method by + * allowing these reserved accelerators to be placed on the + * menu. We will only do this for "Ctrl+Shift+[0-9A-FU]". + */ + final String commandId = updatedAction + .getActionDefinitionId(); + if ((Util.isGtk()) && (callback instanceof IBindingManagerCallback) + && (commandId != null)) { + final IBindingManagerCallback bindingManagerCallback = (IBindingManagerCallback) callback; + final IKeyLookup lookup = KeyLookupFactory.getDefault(); + final TriggerSequence[] triggerSequences = bindingManagerCallback + .getActiveBindingsFor(commandId); + for (int i = 0; i < triggerSequences.length; i++) { + final TriggerSequence triggerSequence = triggerSequences[i]; + final Trigger[] triggers = triggerSequence + .getTriggers(); + if (triggers.length == 1) { + final Trigger trigger = triggers[0]; + if (trigger instanceof KeyStroke) { + final KeyStroke currentKeyStroke = (KeyStroke) trigger; + final int currentNaturalKey = currentKeyStroke + .getNaturalKey(); + if ((currentKeyStroke.getModifierKeys() == (lookup + .getCtrl() | lookup.getShift())) + && ((currentNaturalKey >= '0' && currentNaturalKey <= '9') + || (currentNaturalKey >= 'A' && currentNaturalKey <= 'F') || (currentNaturalKey == 'U'))) { + accelerator = currentKeyStroke + .getModifierKeys() + | currentNaturalKey; + acceleratorText = triggerSequence + .format(); + break; + } + } + } + } + } + + if (accelerator == 0) { + if ((callback != null) && (commandId != null)) { + acceleratorText = callback + .getAcceleratorText(commandId); + } + } + + IContributionManagerOverrides overrides = null; + + if (getParent() != null) { + overrides = getParent().getOverrides(); + } + + if (overrides != null) { + text = getParent().getOverrides().getText(this); + } + + mi.setAccelerator(accelerator); + + if (text == null) { + text = updatedAction.getText(); + } + + if (text != null && acceleratorText == null) { + // use extracted accelerator text in case accelerator + // cannot be fully represented in one int (e.g. + // multi-stroke keys) + acceleratorText = LegacyActionTools + .extractAcceleratorText(text); + if (acceleratorText == null && accelerator != 0) { + acceleratorText = Action + .convertAccelerator(accelerator); + } + } + + if (text == null) { + text = ""; //$NON-NLS-1$ + } else { + text = Action.removeAcceleratorText(text); + } + + if (acceleratorText == null) { + mi.setText(text); + } else { + mi.setText(text + '\t' + acceleratorText); + } + } + + if (imageChanged) { + updateImages(false); + } + + if (enableStateChanged) { + boolean shouldBeEnabled = action.isEnabled() + && isEnabledAllowed(); + + if (mi.getEnabled() != shouldBeEnabled) { + mi.setEnabled(shouldBeEnabled); + } + } + + if (checkChanged) { + boolean bv = action.isChecked(); + + if (mi.getSelection() != bv) { + mi.setSelection(bv); + } + } + + return; + } + + if (widget instanceof Button) { + Button button = (Button) widget; + + if (imageChanged) { + updateImages(false); + } + + if (textChanged) { + String text = action.getText(); + boolean showText = text != null && ((getMode() & MODE_FORCE_TEXT) != 0 || !hasImages(action)); + // only do the trimming if the text will be used + if (showText) { + text = Action.removeAcceleratorText(text); + } + String textToSet = showText ? text : ""; //$NON-NLS-1$ + button.setText(textToSet); + } + + if (tooltipTextChanged) { + button.setToolTipText(action.getToolTipText()); + } + + if (enableStateChanged) { + boolean shouldBeEnabled = action.isEnabled() + && isEnabledAllowed(); + + if (button.getEnabled() != shouldBeEnabled) { + button.setEnabled(shouldBeEnabled); + } + } + + if (checkChanged) { + boolean bv = action.isChecked(); + + if (button.getSelection() != bv) { + button.setSelection(bv); + } + } + return; + } + } + } + + /** + * Updates the images for this action. + * + * @param forceImage + * true if some form of image is compulsory, and + * false if it is acceptable for this item to have + * no image + * @return true if there are images for this action, + * false if not + */ + private boolean updateImages(boolean forceImage) { + + ResourceManager parentResourceManager = JFaceResources.getResources(); + + if (widget instanceof ToolItem) { + if (USE_COLOR_ICONS) { + ImageDescriptor image = action.getHoverImageDescriptor(); + if (image == null) { + image = action.getImageDescriptor(); + } + ImageDescriptor disabledImage = action + .getDisabledImageDescriptor(); + + // Make sure there is a valid image. + if (image == null && forceImage) { + image = ImageDescriptor.getMissingImageDescriptor(); + } + + LocalResourceManager localManager = new LocalResourceManager( + parentResourceManager); + + // performance: more efficient in SWT to set disabled and hot + // image before regular image + ((ToolItem) widget) + .setDisabledImage(disabledImage == null ? null + : localManager + .createImageWithDefault(disabledImage)); + ((ToolItem) widget).setImage(image == null ? null + : localManager.createImageWithDefault(image)); + + disposeOldImages(); + imageManager = localManager; + + return image != null; + } + ImageDescriptor image = action.getImageDescriptor(); + ImageDescriptor hoverImage = action.getHoverImageDescriptor(); + ImageDescriptor disabledImage = action.getDisabledImageDescriptor(); + + // If there is no regular image, but there is a hover image, + // convert the hover image to gray and use it as the regular image. + if (image == null && hoverImage != null) { + image = ImageDescriptor.createWithFlags(action + .getHoverImageDescriptor(), SWT.IMAGE_GRAY); + } else { + // If there is no hover image, use the regular image as the + // hover image, + // and convert the regular image to gray + if (hoverImage == null && image != null) { + hoverImage = image; + image = ImageDescriptor.createWithFlags(action + .getImageDescriptor(), SWT.IMAGE_GRAY); + } + } + + // Make sure there is a valid image. + if (hoverImage == null && image == null && forceImage) { + image = ImageDescriptor.getMissingImageDescriptor(); + } + + // Create a local resource manager to remember the images we've + // allocated for this tool item + LocalResourceManager localManager = new LocalResourceManager( + parentResourceManager); + + // performance: more efficient in SWT to set disabled and hot image + // before regular image + ((ToolItem) widget).setDisabledImage(disabledImage == null ? null + : localManager.createImageWithDefault(disabledImage)); + ((ToolItem) widget).setHotImage(hoverImage == null ? null + : localManager.createImageWithDefault(hoverImage)); + ((ToolItem) widget).setImage(image == null ? null : localManager + .createImageWithDefault(image)); + + // Now that we're no longer referencing the old images, clear them + // out. + disposeOldImages(); + imageManager = localManager; + + return image != null; + } else if (widget instanceof Item || widget instanceof Button) { + + // Use hover image if there is one, otherwise use regular image. + ImageDescriptor image = action.getHoverImageDescriptor(); + if (image == null) { + image = action.getImageDescriptor(); + } + // Make sure there is a valid image. + if (image == null && forceImage) { + image = ImageDescriptor.getMissingImageDescriptor(); + } + + // Create a local resource manager to remember the images we've + // allocated for this widget + LocalResourceManager localManager = new LocalResourceManager( + parentResourceManager); + + if (widget instanceof Item) { + ((Item) widget).setImage(image == null ? null : localManager + .createImageWithDefault(image)); + } else if (widget instanceof Button) { + ((Button) widget).setImage(image == null ? null : localManager + .createImageWithDefault(image)); + } + + // Now that we're no longer referencing the old images, clear them + // out. + disposeOldImages(); + imageManager = localManager; + + return image != null; + } + return false; + } + + /** + * Dispose any images allocated for this contribution item + */ + private void disposeOldImages() { + if (imageManager != null) { + imageManager.dispose(); + imageManager = null; + } + } + + /** + * Shorten the given text t so that its length doesn't exceed + * the width of the given ToolItem.The default implementation replaces + * characters in the center of the original string with an ellipsis ("..."). + * Override if you need a different strategy. + * + * @param textValue + * the text to shorten + * @param item + * the tool item the text belongs to + * @return the shortened string + * + */ + protected String shortenText(String textValue, ToolItem item) { + if (textValue == null) { + return null; + } + + GC gc = new GC(item.getParent()); + + int maxWidth = item.getImage().getBounds().width * 4; + + if (gc.textExtent(textValue).x < maxWidth) { + gc.dispose(); + return textValue; + } + + for (int i = textValue.length(); i > 0; i--) { + String test = textValue.substring(0, i); + test = test + ellipsis; + if (gc.textExtent(test).x < maxWidth) { + gc.dispose(); + return test; + } + + } + gc.dispose(); + // If for some reason we fall through abort + return textValue; + } + + @Override + public void dispose() { + if (widget != null) { + widget.dispose(); + widget = null; + } + holdMenu = null; + } + + /** + * Handle show and hide on the proxy menu for IAction.AS_DROP_DOWN_MENU + * actions. + * + * @return the appropriate listener + * @since 3.4 + */ + private Listener getMenuCreatorListener() { + if (menuCreatorListener == null) { + menuCreatorListener = new Listener() { + @Override + public void handleEvent(Event event) { + switch (event.type) { + case SWT.Show: + handleShowProxy((Menu) event.widget); + break; + case SWT.Hide: + handleHideProxy((Menu) event.widget); + break; + } + } + }; + } + return menuCreatorListener; + } + + /** + * This is the easiest way to hold the menu until we can swap it in to the + * proxy. + */ + private Menu holdMenu = null; + + private boolean menuCreatorCalled = false; + + /** + * The proxy menu is being shown, we better get the real menu. + * + * @param proxy + * the proxy menu + * @since 3.4 + */ + private void handleShowProxy(Menu proxy) { + proxy.removeListener(SWT.Show, getMenuCreatorListener()); + IMenuCreator mc = action.getMenuCreator(); + menuCreatorCalled = true; + if (mc == null) { + return; + } + holdMenu = mc.getMenu(proxy.getParentMenu()); + if (holdMenu == null) { + return; + } + copyMenu(holdMenu, proxy); + } + + /** + * Create MenuItems in the proxy menu that can execute the real menu items + * if selected. Create proxy menus for any real item submenus. + * + * @param realMenu + * the real menu to copy from + * @param proxy + * the proxy menu to populate + * @since 3.4 + */ + private void copyMenu(Menu realMenu, Menu proxy) { + if (realMenu.isDisposed() || proxy.isDisposed()) { + return; + } + + // we notify the real menu so it can populate itself if it was + // listening for SWT.Show + realMenu.notifyListeners(SWT.Show, null); + + final Listener passThrough = new Listener() { + @Override + public void handleEvent(Event event) { + if (!event.widget.isDisposed()) { + Widget realItem = (Widget) event.widget.getData(); + if (!realItem.isDisposed()) { + int style = event.widget.getStyle(); + if (event.type == SWT.Selection + && ((style & (SWT.TOGGLE | SWT.CHECK | SWT.RADIO)) != 0) + && realItem instanceof MenuItem) { + ((MenuItem) realItem) + .setSelection(((MenuItem) event.widget) + .getSelection()); + } + event.widget = realItem; + realItem.notifyListeners(event.type, event); + } + } + } + }; + + MenuItem[] items = realMenu.getItems(); + for (int i = 0; i < items.length; i++) { + final MenuItem realItem = items[i]; + final MenuItem proxyItem = new MenuItem(proxy, realItem.getStyle()); + proxyItem.setData(realItem); + proxyItem.setAccelerator(realItem.getAccelerator()); + proxyItem.setEnabled(realItem.getEnabled()); + proxyItem.setImage(realItem.getImage()); + proxyItem.setSelection(realItem.getSelection()); + proxyItem.setText(realItem.getText()); + + // pass through any events + proxyItem.addListener(SWT.Selection, passThrough); + proxyItem.addListener(SWT.Arm, passThrough); + proxyItem.addListener(SWT.Help, passThrough); + + final Menu itemMenu = realItem.getMenu(); + if (itemMenu != null) { + // create a proxy for any sub menu items + final Menu subMenu = new Menu(proxy); + subMenu.setData(itemMenu); + proxyItem.setMenu(subMenu); + subMenu.addListener(SWT.Show, new Listener() { + @Override + public void handleEvent(Event event) { + event.widget.removeListener(SWT.Show, this); + if (event.type == SWT.Show) { + copyMenu(itemMenu, subMenu); + } + } + }); + subMenu.addListener(SWT.Help, passThrough); + subMenu.addListener(SWT.Hide, passThrough); + } + } + } + + /** + * The proxy menu is being hidden, so we need to make it go away. + * + * @param proxy + * the proxy menu + * @since 3.4 + */ + private void handleHideProxy(final Menu proxy) { + proxy.removeListener(SWT.Hide, getMenuCreatorListener()); + proxy.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + if (!proxy.isDisposed()) { + MenuItem parentItem = proxy.getParentItem(); + proxy.dispose(); + parentItem.setMenu(holdMenu); + } + if (holdMenu != null && !holdMenu.isDisposed()) { + holdMenu.notifyListeners(SWT.Hide, null); + } + holdMenu = null; + } + }); + } + + /** + * Return the widget associated with this contribution item. It should not + * be cached, as it can be disposed and re-created by its containing + * ContributionManager, which controls all of the widgets lifecycle methods. + *

+ * This can be used to set layout data on the widget if appropriate. The + * actual type of the widget can be any valid control for this + * ContributionItem's current ContributionManager. + *

+ * + * @return the widget, or null depending on the lifecycle. + * @since 3.4 + */ + public Widget getWidget() { + return widget; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/TreeAwareFilter.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/TreeAwareFilter.java new file mode 100644 index 000000000..1dedfd5fd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/TreeAwareFilter.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.util; + +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springsource.ide.eclipse.commons.livexp.util.Filter; + +/** + * A wrapper around a Filter, adapting the filter to become 'tree aware'. + *

+ * The resulting filter uses these two rules: + *

+ * a) If the base-filter accepts a child, then the resulting filter must also + * accept the child's parent (and grandparent etc.) + *

+ * b) If the base-filter wrapped filter accepts a (parent) node, then the resulting filter + * must also accept all the children (and grandchildren etc.) of that node. + * + * TODO: it feels like this class could be made more abstract and reused for other + * things that are 'tree-like', not just BootDashElements. + * + * @author Kris De Volder + */ +public class TreeAwareFilter implements Filter { + + private Filter baseFilter; + + public TreeAwareFilter(Filter baseFilter) { + this.baseFilter = baseFilter; + } + + @Override + public boolean accept(BootDashElement e) { + return baseAccepts(e) || baseAcceptsChild(e) || baseAcceptsParent(e); + } + + private boolean baseAcceptsParent(BootDashElement e) { + Object _p = e.getParent(); + if (_p instanceof BootDashElement) { + BootDashElement p = (BootDashElement) _p; + return baseAccepts(p) || baseAcceptsParent(p); + } + return false; + } + + private boolean baseAcceptsChild(BootDashElement e) { + for (BootDashElement c : e.getCurrentChildren()) { + if (baseAccepts(c) || baseAcceptsChild(c)) { + return true; + } + } + return false; + } + + private boolean baseAccepts(BootDashElement e) { + return baseFilter.accept(e); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/UiUtil.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/UiUtil.java new file mode 100644 index 000000000..82db22ae8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/UiUtil.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2015 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.util; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +public class UiUtil { + + public static Shell getShell() { + try { + IWorkbench wb = PlatformUI.getWorkbench(); + CompletableFuture shell = new CompletableFuture<>(); + Display d = wb.getDisplay(); + d.syncExec(() -> { + try { + IWorkbenchWindow win = wb.getActiveWorkbenchWindow(); + if (win!=null) { + shell.complete(win.getShell()); + } else { + shell.complete(d.getActiveShell()); + } + } catch (Throwable e) { + shell.completeExceptionally(e); + } + }); + return shell.get(); + } catch (Exception e) { + throw ExceptionUtil.unchecked(e); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/Utils.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/Utils.java new file mode 100644 index 000000000..b34b0bd30 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/Utils.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.util; + +import org.springsource.ide.eclipse.commons.core.util.StringUtil; + +/** + * Utility methods + * + * @author Kris De Volder + */ +public class Utils { + + /** + * Creates http URL string based on host, port and path + * @param host + * @param port + * @param path + * @return the resultant URL + */ + public static String createUrl(String host, int port, String path) { + if (path==null) { + path = ""; + } + if (host!=null) { + if (port>0) { + if (!path.startsWith("/")) { + path = "/" +path; + } + return "http://"+host+":"+port+path; + } + } + return null; + } + + public static String pathJoin(String p1, String p2) { + if (!StringUtil.hasText(p1)) { + return p2; + } + if (!StringUtil.hasText(p2)) { + return p1; + } + while (p1.endsWith("/")) { + p1 = p1.substring(0, p1.length()-1); + } + while (p2.startsWith("/")) { + p2 = p2.substring(1); + } + return p1+"/"+p2; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/SimpleTemplate.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/SimpleTemplate.java new file mode 100644 index 000000000..5e79b7805 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/SimpleTemplate.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2016 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.util.template; + +/** + * Implements a simple Templating language and substitution algorithm. + *

+ * A template is a string containing template variables of the for '%x' where + * x is any single character other than '%'. + *

+ * A '%' can be escaped by preceding by repeating it. I.e. '%%' in the template + * expands to a single '%' in the output. + *

+ * A '%' that occurs at the end of the template is not substituted (i.e. it is automatically + * 'escaped'. + *

+ * If a template variable is not bound (i.e. {@link TemplateEnv} returns null for it) then it is + * not substituted (the variable just remains in the output string unchanged). + * + * @author Kris De Volder + */ +public class SimpleTemplate implements Template { + + /** + * A template variable is a single character preceded by this character. + */ + private final char VAR_CHAR = '%'; + + private String pattern; + + public SimpleTemplate(String pattern) { + this.pattern = pattern; + } + + @Override + public String render(TemplateEnv env) { + int len = pattern.length(); + int nextChar = 0; //position of next input char to read from the pattern. + StringBuilder output = new StringBuilder(); + while (nextChar=0) { + output.append(pattern.substring(nextChar, nextVarAt)); + char varName = getVarName(nextVarAt); + output.append(getValue(varName, env)); + //next char should be right after the processed var, and the var is something like "%u" + nextChar = nextVarAt+2; + } else { + //no more vars + output.append(pattern.substring(nextChar)); + nextChar = len; + } + } + return output.toString(); + } + + private Object getValue(char varName, TemplateEnv env) { + if (varName==VAR_CHAR) { + //its not actually a real var, but an escaped '%' + return VAR_CHAR; + } + String resolved = env.getTemplateVar(varName); + return resolved!=null?resolved:new String(new char[]{VAR_CHAR, varName}); + } + + private char getVarName(int varPos) { + int namePos = varPos+1; + //If there's nothing after a '%' then treat it the same as a escaped '%' (rather than blowing up) + return namePos < pattern.length() ? pattern.charAt(namePos) : VAR_CHAR; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/Template.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/Template.java new file mode 100644 index 000000000..b22a9f61b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/Template.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2016 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.util.template; + +/** + * A template is some kind of pattern containing variables. It can be rendered into + * text by substituting the variables values. + * + * @author Kris De Volder + */ +public interface Template { + /** + * Render this template into text, looking up variables from the given {@link TemplateEnv} + */ + String render(TemplateEnv env); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/TemplateEnv.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/TemplateEnv.java new file mode 100644 index 000000000..409566c1a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/TemplateEnv.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2016 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.util.template; + +/** + * An implementation of this interface provides a means to 'resolve' variable names + * used in rendering a {@link Template}. + * + * @author Kris De Volder + */ +public interface TemplateEnv { + String getTemplateVar(char name); +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/Templates.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/Templates.java new file mode 100644 index 000000000..55b008355 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/util/template/Templates.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2016 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.util.template; + +/** + * Methods for creating Template istances and related stuff. + * + * @author Kris De Volder + */ +public class Templates { + + public static final TemplateEnv NULL_ENV = new TemplateEnv() { + @Override + public String getTemplateVar(char name) { + return null; + } + public String toString() { + return "NULL_ENV"; + } + }; + + public static Template create(String pattern) { + //This could be optimized by 'compiling' pattern into somekind of object-graph + //so that it doesn't actually require parsing and analyzing the pattern each time it gets + //rendered. However... lets keep things simple for now. We aren't using this for huge + //patterns or large amounts so it should be fine. + //Also note that optimizing this wouldn't make make much sense unless the result of calls to + // this method are actually reused more than once. + if (pattern!=null) { + return new SimpleTemplate(pattern); + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AbstractBootDashAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AbstractBootDashAction.java new file mode 100644 index 000000000..c457e5776 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AbstractBootDashAction.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import java.util.EnumSet; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +/** + * @author Kris De Volder + */ +public class AbstractBootDashAction extends Action implements Disposable { + + public static enum Location { + CONTEXT_MENU, + CUSTOMIZE_MENU + } + + private boolean isVisible = true; + protected final SimpleDIContext context; + + public EnumSet showIn() { + return EnumSet.of(Location.CONTEXT_MENU); + } + + protected AbstractBootDashAction(SimpleDIContext context, int style) { + super("", style); + this.context = context; + context.assertDefinitionFor(UserInteractions.class); + } + + protected AbstractBootDashAction(SimpleDIContext context) { + this(context, IAction.AS_UNSPECIFIED); + } + + public void dispose() { + } + + public boolean isVisible() { + return isVisible; + } + + public void setVisible(boolean show) { + this.isVisible = show; + } + + public UserInteractions ui() { + return context.getBean(UserInteractions.class); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AbstractBootDashElementsAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AbstractBootDashElementsAction.java new file mode 100644 index 000000000..4b7a5f15f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AbstractBootDashElementsAction.java @@ -0,0 +1,165 @@ +/******************************************************************************* + * Copyright (c) 2015, 2019 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import java.util.Collection; + +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.PlatformUI; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveProcessCommandsExecutor; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +import com.google.common.collect.ImmutableSet; + +/** + * Abstract super class for BootDash actions that operate on selections + * of elements. + * + * @author Kris De Volder + */ +public class AbstractBootDashElementsAction extends AbstractBootDashAction { + + private static final boolean DEBUG = false;//(""+Platform.getLocation()).contains("kdvolder"); + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + final protected MultiSelection selection; + final private ValueListener> selectionListener; + final protected BootDashViewModel model; + private ElementStateListener modelListener; + + public AbstractBootDashElementsAction(Params params) { + super(params.context, params.style); + this.model = params.model; + this.selection = params.selection; + if (model!=null) { + model.addElementStateListener(modelListener = new ElementStateListener() { + public void stateChanged(BootDashElement e) { + debug("action '"+getText()+"' updating for element "+e); + if (selection.getValue().contains(e) && !PlatformUI.getWorkbench().isClosing()) { + PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { + public void run() { + update(); + } + }); + } + } + + }); + } + selection.getElements().addListener(selectionListener = new ValueListener>() { + public void gotValue(LiveExpression> exp, ImmutableSet selecteds) { + update(); + } + }); + if (params.definitionId != null) { + this.setActionDefinitionId(params.definitionId); + params.actions.bindAction(this); + } + } + + public void update() { + updateEnablement(); + updateVisibility(); + } + + /** + * Subclass can override to compuet enablement differently. + * The default implementation enables if a single element is selected. + */ + public void updateEnablement() { + Collection selecteds = getSelectedElements(); + this.setEnabled(selecteds.size()==1); + } + + public void updateVisibility() { + this.setVisible(getSelectedElements().size() > 0); + } + + public Collection getSelectedElements() { + return selection.getValue(); + } + + protected BootDashElement getSingleSelectedElement() { + return selection.getSingle(); + } + + public void dispose() { + if (selectionListener!=null) { + selection.getElements().removeListener(selectionListener); + } + if (modelListener!=null) { + model.removeElementStateListener(modelListener); + modelListener = null; + } + super.dispose(); + } + + public static class Params { + private BootDashActions actions; + private BootDashViewModel model; + private MultiSelection selection; + private SimpleDIContext context; + private int style = IAction.AS_UNSPECIFIED; + private String definitionId; + private LiveProcessCommandsExecutor liveProcessCmds; + + public Params(BootDashActions actions) { + this.actions = actions; + } + public Params setModel(BootDashViewModel model) { + this.model = model; + return this; + } + public MultiSelection getSelection() { + return selection; + } + public Params setSelection(MultiSelection selection) { + this.selection = selection; + return this; + } + public SimpleDIContext getContext() { + return context; + } + public Params setContext(SimpleDIContext context) { + this.context = context; + return this; + } + public Params setStyle(int style) { + this.style = style; + return this; + } + public Params setDefinitionId(String definitionId) { + this.definitionId = definitionId; + return this; + } + public LiveProcessCommandsExecutor getLiveProcessCmds() { + return liveProcessCmds; + } + public Params setLiveProcessCmds(LiveProcessCommandsExecutor liveProcessCmds) { + this.liveProcessCmds = liveProcessCmds; + return this; + } + public BootDashViewModel getModel() { + return model; + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AbstractBootDashModelAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AbstractBootDashModelAction.java new file mode 100644 index 000000000..007044e6a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AbstractBootDashModelAction.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ModelStateListener; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +/** + * Abstract superclass for actions that operate on a selection of a single + * 'section' element. + * + * @author Kris De Volder + */ +public abstract class AbstractBootDashModelAction extends AbstractBootDashAction { + + final private ModelStateListener STATE_LISTENER = new ModelStateListener() { + @Override + public void stateChanged(BootDashModel model) { + update(); + } + }; + + protected final LiveExpression sectionSelection; + private ValueListener sectionListener; + private BootDashModel listeningTo; + + protected AbstractBootDashModelAction(LiveExpression section, SimpleDIContext context) { + super(context); + this.sectionSelection = section; + this.listeningTo = null; + this.sectionSelection.addListener(sectionListener = new ValueListener() { + public void gotValue(LiveExpression exp, BootDashModel value) { + update(); + } + }); + update(); + } + + private void updateStateListener() { + if (listeningTo != null) { + listeningTo.removeModelStateListener(STATE_LISTENER); + listeningTo = null; + } + if (isVisible()) { + listeningTo = sectionSelection.getValue(); + if (listeningTo != null) { + listeningTo.addModelStateListener(STATE_LISTENER); + } + } + } + + public void update() { + updateVisibility(); + updateEnablement(); + updateStateListener(); + } + + public void updateEnablement() { + this.setEnabled(sectionSelection.getValue()!=null); + } + + public void updateVisibility() { + this.setVisible(sectionSelection.getValue()!=null); + } + + @Override + public void dispose() { + if (listeningTo != null) { + listeningTo.removeModelStateListener(STATE_LISTENER); + listeningTo = null; + } + if (sectionListener!=null) { + sectionSelection.removeListener(sectionListener); + sectionListener = null; + } + super.dispose(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AddRunTargetAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AddRunTargetAction.java new file mode 100644 index 000000000..b1637f2ac --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/AddRunTargetAction.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public class AddRunTargetAction extends AbstractBootDashAction { + + private LiveSetVariable targets; + public final RunTargetType runTargetType; + private CompletableFuture lastRun; + + public AddRunTargetAction(RunTargetType runTargetType, LiveSetVariable targets, SimpleDIContext context) { + super(context); + this.runTargetType = runTargetType; + this.targets = targets; + this.setText("Add a "+runTargetType.getName()+" Target"); + this.setToolTipText("Configure a connection to "+runTargetType.getName()+" and add it as a new section to the Boot Dashboard"); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/add_target.png")); + this.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/add_target_disabled.png")); + setEnabled(true); + setVisible(true); + } + + @Override + public void run() { + Log.async(lastRun = runTargetType.openTargetCreationUi(targets)); + } + + public void waitFor() throws Exception { + lastRun.get(); + } + + /** + * For testing code to allow proper synchronisation (i.e. execute the actiona and + * then wait for the result. + */ + public void waitFor(Duration timeout) throws Exception { + lastRun.get(timeout.toMillis(), TimeUnit.MILLISECONDS); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashActions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashActions.java new file mode 100644 index 000000000..cbf71b5ce --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashActions.java @@ -0,0 +1,645 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.views; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +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.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import org.eclipse.core.runtime.Assert; +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.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.DeployToRemoteTargetAction; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveDataConnectionManagementActions; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveProcessCommandsExecutor; +import org.springframework.ide.eclipse.boot.dash.livexp.DisposingFactory; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElement; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.LocalRunTargetType; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTarget; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTargetType; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKInstallManager; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashAction.Location; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashElementsAction.Params; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; + +public class BootDashActions { + + private final static String PROPERTIES_VIEW_ID = "org.eclipse.ui.views.PropertySheet"; //$NON-NLS-1$ + + ///// context info ////////////// + private BootDashViewModel model; + MultiSelection elementsSelection; + private LiveExpression sectionSelection; + private LiveProcessCommandsExecutor liveProcessCmds; + + ///// actions /////////////////// + private RunStateAction[] runStateActions; + private AbstractBootDashElementsAction openConsoleAction; + private LinkWithConsoleAction linkWithConsoleAction; + private OpenLaunchConfigAction openConfigAction; + private OpenInBrowserAction openBrowserAction; + private OpenNgrokAdminUi openNgrokAdminUi; + private OpenInPackageExplorer openInPackageExplorerAction; + private AddRunTargetAction[] addTargetActions; + private RefreshRunTargetAction refreshAction; + private RemoveRunTargetAction removeTargetAction; + private ShowViewAction showPropertiesViewAction; + private ExposeAppAction exposeRunAppAction; + private ExposeAppAction exposeDebugAppAction; + + private OpenFilterPreferencesAction openFilterPreferencesAction; + + private DuplicateConfigAction duplicateConfigAction; + + private DeleteElementsAction deleteAppsAction; + private DeleteElementsAction deleteConfigsAction; + + private OpenToggleFiltersDialogAction toggleFiltersDialogAction; + private ToggleFilterAction[] toggleFilterActions; + private CustmomizeTargetLabelAction customizeTargetLabelAction; + + private DisposingFactory debugOnTargetActions; + private DisposingFactory runOnTargetActions; + + private Map defIdToActions = new HashMap<>(); + + private LiveDataConnectionManagementActions liveDataConnectionManagement; + private final SimpleDIContext context; + + private List injectedActions; + + private ToggleBootDashModelConnection connectAction; + + private EnableRemoteDevtoolsAction enableRemoteDevtoolsAction; + + private RestartDevtoolsClientAction restartDevtoolsClientAction; + + public interface Factory { + Collection create(BootDashActions actions, BootDashViewModel model, MultiSelection selection, LiveExpression section, SimpleDIContext context, LiveProcessCommandsExecutor liveProcessCmds); + } + + public BootDashActions(BootDashViewModel model, MultiSelection selection, SimpleDIContext context, LiveProcessCommandsExecutor liveProcessCmds) { + this( + model, + selection, + null, + context, + liveProcessCmds + ); + } + + public BootDashActions(BootDashViewModel model, MultiSelection selection, LiveExpression section, SimpleDIContext context, LiveProcessCommandsExecutor liveProcessCmds) { + this.liveProcessCmds = liveProcessCmds; + Assert.isNotNull(context); + context.assertDefinitionFor(UserInteractions.class); + this.model = model; + this.elementsSelection = selection; + this.sectionSelection = section; + this.context = context; + + makeActions(); + } + + private Params defaultActionParams() { + return new Params(this) + .setModel(model) + .setSelection(elementsSelection) + .setContext(context) + .setLiveProcessCmds(liveProcessCmds); + } + + protected void makeActions() { + RunStateAction restartAction = new RestartAction(defaultActionParams().setDefinitionId("org.springframework.ide.eclipse.boot.dash.boot.dash.RestartAction"), RunState.RUNNING); + + RunStateAction rebugAction = new RedebugAction(defaultActionParams().setDefinitionId("org.springframework.ide.eclipse.boot.dash.boot.dash.RedebugAction"), RunState.DEBUGGING); + + RunStateAction stopAction = new RunStateAction(defaultActionParams().setDefinitionId("org.springframework.ide.eclipse.boot.dash.boot.dash.StopAction"), RunState.INACTIVE) { + @Override + protected boolean currentStateAcceptable(RunState s) { + // Enable stop button so CF apps can be stopped when "STARTING" + return s != RunState.INACTIVE; + } + + @Override + protected Job createJob() { + final Collection selecteds = elementsSelection.getValue(); + if (!selecteds.isEmpty()) { + return new Job("Stopping " + selecteds.size() + " Boot Dash Elements") { + protected IStatus run(IProgressMonitor monitor) { + monitor.beginTask("Stopping " + selecteds.size() + " Elements", selecteds.size()); + try { + + List> futures = new ArrayList<>(selecteds.size()); + for (BootDashElement el : selecteds) { + futures.add(CompletableFuture.runAsync(() -> { + try { + el.stop(); + monitor.worked(1); + } catch (Exception e) { + monitor.worked(1); + throw new CompletionException(e); + } + })); + } + + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])).get(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + BootActivator.createErrorStatus(e); + } catch (ExecutionException e) { + BootActivator.createErrorStatus(e); + } catch (TimeoutException e) { + BootActivator.createErrorStatus(e); + } + + + return Status.OK_STATUS; + } finally { + monitor.done(); + } + } + }; + } + return null; + } + }; + stopAction.setText("Stop"); + stopAction.setToolTipText("Stop the process(es) associated with the selected elements"); + stopAction.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/stop.png")); + stopAction.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/stop_disabled.png")); + + RunStateAction pauseAction = new RunStateAction(defaultActionParams(), RunState.PAUSED) { + @Override + protected boolean isVisibleForElement(BootDashElement e) { + return RunState.PAUSED != e.getRunState() && super.isVisibleForElement(e); + } + + @Override + protected boolean currentStateAcceptable(RunState s) { + return s.isActive(); + } + + @Override + public boolean showInToolbar() { + return false; + } + }; + pauseAction.setText("Pause"); + pauseAction.setToolTipText("Suspend the process(es) associated with the selected elements."); + pauseAction.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/suspend.gif")); + pauseAction.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/suspend_disabled.gif")); + + RunStateAction resumeRunAction = new RunStateAction(defaultActionParams(), RunState.RUNNING) { + @Override + protected boolean currentStateAcceptable(RunState s) { + return s == RunState.PAUSED; + } + + @Override + protected boolean isVisibleForElement(BootDashElement e) { + // Only show Resume action for elements that can be paused explicitly + return e.supportedGoalStates().contains(RunState.PAUSED) && super.isVisibleForElement(e); + } + + @Override + public boolean showInToolbar() { + return false; + } + }; + resumeRunAction.setText("Resume (Running)"); + resumeRunAction.setToolTipText("Resume previously suspended process(es) associated with the selected elements."); + resumeRunAction.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/resume.gif")); + resumeRunAction.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/resumed.gif")); + + + RunStateAction resumeDebugAction = new RunStateAction(defaultActionParams(), RunState.DEBUGGING) { + @Override + protected boolean currentStateAcceptable(RunState s) { + return s == RunState.PAUSED; + } + + @Override + public boolean showInToolbar() { + return false; + } + }; + resumeDebugAction.setText("Resume (Debugging)"); + resumeDebugAction.setToolTipText("Resume previously suspended process(es) associated with the selected elements."); + resumeDebugAction.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/resume.gif")); + resumeDebugAction.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/resumed.gif")); + + runStateActions = new RunStateAction[] { restartAction, rebugAction, stopAction, pauseAction, resumeRunAction, resumeDebugAction }; + + openConfigAction = new OpenLaunchConfigAction(defaultActionParams().setDefinitionId("org.springframework.ide.eclipse.boot.dash.boot.dash.OpenLaunchConfigAction")); + openConsoleAction = new OpenConsoleAction(defaultActionParams()); + linkWithConsoleAction = new LinkWithConsoleAction(defaultActionParams().setStyle(IAction.AS_CHECK_BOX)); + openBrowserAction = new OpenInBrowserAction(defaultActionParams()); + openNgrokAdminUi = new OpenNgrokAdminUi(defaultActionParams()); + openInPackageExplorerAction = new OpenInPackageExplorer(defaultActionParams()); + addTargetActions = createAddTargetActions(); + + deleteAppsAction = new DeleteElementsAction<>(this, RemoteRunTargetType.class, elementsSelection, context); + deleteAppsAction.setText("Delete"); + deleteAppsAction.setToolTipText("Permantently removes selected artifact(s) from Remote Target"); + deleteConfigsAction = new DeleteElementsAction<>(this, LocalRunTargetType.class, elementsSelection, context); + deleteConfigsAction.setText("Delete Config"); + deleteConfigsAction.setToolTipText("Permantently deletes Launch Configgurations from the workspace"); + + if (sectionSelection != null) { + connectAction = new ToggleBootDashModelConnection(sectionSelection, context); + refreshAction = new RefreshRunTargetAction(sectionSelection, context); + removeTargetAction = new RemoveRunTargetAction(sectionSelection, model, context); + customizeTargetLabelAction = new CustmomizeTargetLabelAction(sectionSelection, context); + } + + showPropertiesViewAction = new ShowViewAction(PROPERTIES_VIEW_ID); + + toggleFiltersDialogAction = new OpenToggleFiltersDialogAction(model.getToggleFilters(), elementsSelection, context); + toggleFilterActions = new ToggleFilterAction[model.getToggleFilters().getAvailableFilters().length]; + for (int i = 0; i < toggleFilterActions.length; i++) { + toggleFilterActions[i] = new ToggleFilterAction(model, model.getToggleFilters().getAvailableFilters()[i], context); + } + + exposeRunAppAction = new ExposeAppAction(defaultActionParams(), RunState.RUNNING, NGROKInstallManager.getInstance()); + exposeRunAppAction.setText("(Re)start and Expose via ngrok"); + exposeRunAppAction.setToolTipText("Start or restart the process associated with the selected elements and expose it to the outside world via an ngrok tunnel"); + exposeRunAppAction.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/restart.png")); + exposeRunAppAction.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/restart_disabled.png")); + + exposeDebugAppAction = new ExposeAppAction(defaultActionParams(), RunState.DEBUGGING, NGROKInstallManager.getInstance()); + exposeDebugAppAction.setText("(Re)debug and Expose via ngrok"); + exposeDebugAppAction.setToolTipText("Start or restart the process associated with the selected elements in debug mode and expose it to the outside world via an ngrok tunnel"); + exposeDebugAppAction.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/rebug.png")); + exposeDebugAppAction.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/rebug_disabled.png")); + + duplicateConfigAction = new DuplicateConfigAction(defaultActionParams()); + + debugOnTargetActions = createDeployOnTargetActions(RunState.DEBUGGING); + runOnTargetActions = createDeployOnTargetActions(RunState.RUNNING); + + openFilterPreferencesAction = new OpenFilterPreferencesAction(context); + liveDataConnectionManagement = new LiveDataConnectionManagementActions(defaultActionParams()); + + enableRemoteDevtoolsAction = new EnableRemoteDevtoolsAction(defaultActionParams()); + restartDevtoolsClientAction = new RestartDevtoolsClientAction(defaultActionParams()); + + ImmutableList.Builder injectedActions = ImmutableList.builder(); + for (Factory f : context.getBeans(Factory.class)) { + injectedActions.addAll(f.create(this, model, elementsSelection, sectionSelection, context, liveProcessCmds)); + }; + this.injectedActions = injectedActions.build(); + } + + private AddRunTargetAction[] createAddTargetActions() { + Set targetTypes = model.getRunTargetTypes(); + ArrayList actions = new ArrayList<>(); + for (RunTargetType tt : targetTypes) { + if (tt.canInstantiate()) { + actions.add(new AddRunTargetAction(tt, model.getRunTargets(), context)); + } + } + return actions.toArray(new AddRunTargetAction[actions.size()]); + } + + private static final class RestartAction extends RunOrDebugStateAction { + private RestartAction(Params params, RunState goalState) { + super(params, goalState); + setText("(Re)start"); + setToolTipText("Start or restart the process associated with the selected elements"); + setImageDescriptor(BootDashActivator.getImageDescriptor("icons/restart.png")); + setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/restart_disabled.png")); + } + } + + private static final class RedebugAction extends RunOrDebugStateAction { + private RedebugAction(Params params, RunState goalState) { + super(params, goalState); + setText("(Re)debug"); + setToolTipText("Start or restart the process associated with the selected elements in debug mode"); + setImageDescriptor(BootDashActivator.getImageDescriptor("icons/rebug.png")); + setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/rebug_disabled.png")); + } + } + + public static class RunOrDebugStateAction extends RunStateAction { + + public RunOrDebugStateAction(Params params, RunState goalState) { + super(params, goalState); + Assert.isLegal(goalState == RunState.RUNNING || goalState == RunState.DEBUGGING); + } + + @Override + protected Job createJob() { + final Collection selecteds = getTargetElements(); + if (!selecteds.isEmpty()) { + return new Job("Restarting " + selecteds.size() + " Dash Elements") { + @Override + public IStatus run(IProgressMonitor monitor) { + monitor.beginTask("Restart Boot Dash Elements", selecteds.size()); + try { + for (BootDashElement el : selecteds) { + monitor.subTask("Restarting: " + el.getName()); + try { + el.restart(goalState, ui()); + } catch (Exception e) { + return BootActivator.createErrorStatus(e); + } + monitor.worked(1); + } + return Status.OK_STATUS; + } finally { + monitor.done(); + } + } + + }; + } + return null; + } + + /** + * Automatically retarget this action to apply to all the children of an element + * (if it has children). This way the action behaves logically if both a parent and some children + * are selected (i.e. we don't want to execute the action twice on the explicitly selected children!) + */ + public Collection getTargetElements() { + Builder builder = ImmutableSet.builder(); + addTargetsFor(builder, getSelectedElements()); + return builder.build(); + } + + private void addTargetsFor(Builder builder, Collection selecteds) { + for (BootDashElement s : selecteds) { + addTargetsFor(builder, s); + } + } + + private void addTargetsFor(Builder builder, BootDashElement s) { + if (s instanceof GenericRemoteAppElement) { //TODO: yuck!!! smelly. + // should refactor so that the special logci below which is + // applicale only to local elelements is somehow moved into that instead. + builder.add(s); + } else { + ImmutableSet children = s.getChildren().getValues(); + if (children.isEmpty()) { + //No children, add s itself + builder.add(s); + } else { + addTargetsFor(builder, children); + } + } + } + } + + public RunStateAction[] getRunStateActions() { + return runStateActions; + } + + public AbstractBootDashElementsAction getOpenBrowserAction() { + return openBrowserAction; + } + + public AbstractBootDashElementsAction getOpenNgrokAdminUi() { + return openNgrokAdminUi; + } + + public AbstractBootDashElementsAction getOpenConsoleAction() { + return openConsoleAction; + } + + public AbstractBootDashElementsAction getLinkWithConsoleAction() { + return linkWithConsoleAction; + } + + public AbstractBootDashElementsAction getOpenInPackageExplorerAction() { + return openInPackageExplorerAction; + } + + public OpenLaunchConfigAction getOpenConfigAction() { + return openConfigAction; + } + + public AddRunTargetAction[] getAddRunTargetActions() { + return addTargetActions; + } + + public ToggleBootDashModelConnection getConnectAction() { + return connectAction; + } + + public IAction getRemoveRunTargetAction() { + return removeTargetAction; + } + + /** + * @return May be null as it may not be supported on all models. + */ + public IAction getRefreshRunTargetAction() { + return refreshAction; + } + + public DeleteElementsAction getDeleteAppsAction() { + return deleteAppsAction; + } + + public IAction getDeleteConfigsAction() { + return deleteConfigsAction; + } + + /** + * @return show properties view action instance + */ + public IAction getShowPropertiesViewAction() { + return showPropertiesViewAction; + } + + public IAction getExposeRunAppAction() { + return exposeRunAppAction; + } + + public IAction getExposeDebugAppAction() { + return exposeDebugAppAction; + } + + public EnableRemoteDevtoolsAction getEnableDevtoolsAction() { + return enableRemoteDevtoolsAction; + } + + public RestartDevtoolsClientAction getRestartDevtoolsClientAction() { + return restartDevtoolsClientAction; + } + + public void dispose() { + if (runStateActions != null) { + for (RunStateAction a : runStateActions) { + a.dispose(); + } + runStateActions = null; + } + if (openConsoleAction != null) { + openConsoleAction.dispose(); + } + if (linkWithConsoleAction != null) { + linkWithConsoleAction.dispose(); + } + if (openConfigAction != null) { + openConfigAction.dispose(); + } + if (openBrowserAction != null) { + openBrowserAction.dispose(); + } + if (addTargetActions != null) { + for (AddRunTargetAction a : addTargetActions) { + a.dispose(); + } + addTargetActions = null; + } + if (toggleFiltersDialogAction != null) { + toggleFiltersDialogAction.dispose(); + toggleFiltersDialogAction = null; + } + + if (exposeRunAppAction != null) { + exposeRunAppAction.dispose(); + exposeRunAppAction = null; + } + + if (exposeDebugAppAction != null) { + exposeDebugAppAction.dispose(); + exposeDebugAppAction = null; + } + if (duplicateConfigAction != null) { + duplicateConfigAction.dispose(); + duplicateConfigAction = null; + } + if (toggleFilterActions!=null) { + for (ToggleFilterAction a : toggleFilterActions) { + a.dispose(); + } + toggleFilterActions = null; + } + debugOnTargetActions.dispose(); + runOnTargetActions.dispose(); + liveDataConnectionManagement.dispose(); + } + + public IAction getToggleFiltersDialogAction() { + return toggleFiltersDialogAction; + } + + public DuplicateConfigAction getDuplicateConfigAction() { + return duplicateConfigAction; + } + + public ToggleFilterAction[] getToggleFilterActions() { + return toggleFilterActions; + } + + public CustmomizeTargetLabelAction getCustomizeTargetLabelAction() { + return customizeTargetLabelAction; + } + + public ImmutableList getDebugOnTargetActions() { + return getDeployAndStartOnTargetActions(debugOnTargetActions); + } + public ImmutableList getRunOnTargetActions() { + return getDeployAndStartOnTargetActions(runOnTargetActions); + } + + public OpenFilterPreferencesAction getOpenFilterPreferencesAction() { + return openFilterPreferencesAction; + } + + private ImmutableList getDeployAndStartOnTargetActions( + DisposingFactory actionFactory) { + ArrayList targets = new ArrayList<>(model.getRunTargets().getValues()); + Collections.sort(targets, model.getTargetComparator()); + + ImmutableList.Builder builder = ImmutableList.builder(); + for (RunTarget target : targets) { + if (target.getType() instanceof RemoteRunTargetType) { + AbstractBootDashAction a = actionFactory.createOrGet(target); + if (a!=null) { + builder.add(actionFactory.createOrGet(target)); + } + } + } + return builder.build(); + } + + private DisposingFactory createDeployOnTargetActions(final RunState runningOrDebugging) { + ObservableSet runtargets = model.getRunTargets(); + return new DisposingFactory(runtargets) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + protected AbstractBootDashAction create(RunTarget target) { + if (target instanceof RemoteRunTarget) { + return new DeployToRemoteTargetAction(defaultActionParams(), (RemoteRunTarget)target, runningOrDebugging); + } + return null; + } + }; + } + + void bindAction(AbstractBootDashElementsAction action) { + Assert.isTrue(!defIdToActions .containsKey(action.getActionDefinitionId()), "Duplicate action definition id " + action.getActionDefinitionId()); + defIdToActions.put(action.getActionDefinitionId(), action); + } + + public IAction getAction(String id) { + return defIdToActions.get(id); + } + + public LiveDataConnectionManagementActions getLiveDataConnectionManagement() { + return liveDataConnectionManagement; + } + + UserInteractions ui() { + return context.getBean(UserInteractions.class); + } + + public List getInjectedActions(Location menu) { + return injectedActions.stream().filter(a -> a.showIn().contains(menu)).collect(Collectors.toList()); + } + + public Collection getAllInjectedActions() { + return injectedActions; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashCellLabelProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashCellLabelProvider.java new file mode 100644 index 000000000..e23813365 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashCellLabelProvider.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.views; + +import org.eclipse.jface.viewers.ColumnViewer; +import org.eclipse.jface.viewers.StyledCellLabelProvider; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.RefreshState; +import org.springframework.ide.eclipse.boot.dash.util.ColumnViewerAnimator; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +/** + * @author Kris De Volder + */ +public class BootDashCellLabelProvider extends StyledCellLabelProvider { + + protected final BootDashColumn forColum; + private ColumnViewerAnimator animator; + private BootDashLabels bdeLabels; + + private ColumnViewer tv; + + public BootDashCellLabelProvider(ColumnViewer tv, BootDashColumn target, Stylers stylers) { + this.tv = tv; + this.forColum = target; + this.bdeLabels = new BootDashLabels(BootDashActivator.getDefault().getInjections(), stylers); + } + + @Override + public void update(ViewerCell cell) { + Object e = cell.getElement(); + Image[] imgs = bdeLabels.getImageAnimation(e, forColum); + StyledString label = bdeLabels.getStyledText(e, forColum); + cell.setText(label.getString()); + cell.setStyleRanges(label.getStyleRanges()); + animate(cell, imgs); + } + + private void animate(ViewerCell cell, Image[] images) { + if (animator==null) { + animator = new ColumnViewerAnimator(tv); + } + animator.setAnimation(cell, images); + } + + @Override + public void dispose() { + super.dispose(); + bdeLabels.dispose(); + if (animator!=null) { + animator.dispose(); + animator = null; + } + } + + @Override + public String getToolTipText(Object element) { + if (element instanceof BootDashModel) { + RefreshState state = ((BootDashModel) element).getRefreshState(); + if (state!=null) { + return state.getMessage(); + } + } + if (element instanceof BootDashElement) { + RefreshState state = ((BootDashElement) element).getRefreshState(); + if (state!=null) { + return state.getMessage(); + } + } + return null; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashContentProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashContentProvider.java new file mode 100644 index 000000000..cd572b3f8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashContentProvider.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views; + +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; + +/** + * @author Kris De Volder + */ +public class BootDashContentProvider implements IStructuredContentProvider { + + private BootDashModel model; + + public BootDashContentProvider(BootDashModel model) { + this.model = model; + } + public void inputChanged(Viewer v, Object oldInput, Object newInput) { + } + public void dispose() { + //Actually we don't own the model but 'borrow' it. So don't dispose + } + public Object[] getElements(Object parent) { + Object[] es = model.getElements().getValue().toArray(); + return es; + } +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashHandler.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashHandler.java new file mode 100644 index 000000000..831f154a4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashHandler.java @@ -0,0 +1,35 @@ +package org.springframework.ide.eclipse.boot.dash.views; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.IViewPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; + +public class BootDashHandler extends AbstractHandler { + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + BootDashActions actions = getActions(); + if (actions != null) { + IAction action = actions.getAction(event.getCommand().getId()); + if (action != null && action.isEnabled()) { + action.run(); + } + } + return null; + } + + private BootDashActions getActions() { + for (IWorkbenchPage page : PlatformUI.getWorkbench().getActiveWorkbenchWindow().getPages()) { + IViewPart view = page.findView(BootDashTreeView.ID); + if (view instanceof BootDashTreeView) { + return ((BootDashTreeView)view).getActions(); + } + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashModelConsoleManager.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashModelConsoleManager.java new file mode 100644 index 000000000..6ebcb8847 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashModelConsoleManager.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.views; + +import java.io.StringWriter; +import java.text.DateFormat; +import java.util.Date; + +import org.eclipse.ui.console.IConsole; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.console.LogType; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; + +/** + * Console manager for elements in a {@link BootDashModel}. + *

+ * Each {@link BootDashModel} should have its own console manager. + * + */ +public abstract class BootDashModelConsoleManager { + + /** + * Opens the console for the given element. If console does not exist, a new + * one will be created. + * + * @param element + * @throws Exception + * if failure occurred while opening console (e.g. failed to + * create console, underlying process is terminated, etc..) + */ + public abstract void showConsole(App element) throws Exception; + + /** + * Write a message to an EXISTING console for the associated element. + * + * @param element + * @param message + */ + public void writeToConsole(App element, String message, LogType type) throws Exception { + if (message != null) { + String bootMessage = asBootDashLog(message); + doWriteToConsole(element, bootMessage, type); + } + } + + protected abstract void doWriteToConsole(App element, String bootDashMessage, LogType type) + throws Exception; + + /** + * Resets console (including possibly clearing contents) without destroying + * the console. + *

+ * This allows "active" consoles to remain alive but display updated + * information + * + * @param appName + */ + public abstract void resetConsole(App element); + + public abstract void terminateConsole(App element) throws Exception; + + public abstract void reconnect(App element) throws Exception; + + public abstract IConsole safeGetOrCreateConsole(App element); + + protected String asBootDashLog(String message) { + Date date = new Date(System.currentTimeMillis()); + String dateVal = DateFormat.getDateTimeInstance().format(date); + StringWriter writer = new StringWriter(); + writer.append('['); + writer.append(dateVal); + writer.append(' '); + writer.append('-'); + writer.append(' '); + writer.append("Boot Dashboard"); + writer.append(']'); + writer.append(' '); + writer.append('-'); + writer.append(' '); + writer.append(message); + writer.append('\n'); + return writer.toString(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashTreeView.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashTreeView.java new file mode 100644 index 000000000..2a87574a0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/BootDashTreeView.java @@ -0,0 +1,364 @@ +/******************************************************************************* + * 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.views; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuCreator; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.ISelectionProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.views.properties.IPropertySheetPage; +import org.eclipse.ui.views.properties.tabbed.ITabbedPropertySheetPageContributor; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveProcessCommandsExecutor; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelectionSource; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.util.MenuUtil; +import org.springframework.ide.eclipse.boot.dash.util.ToolbarPulldownContributionItem; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashUnifiedTreeSection; +import org.springframework.ide.eclipse.boot.dash.views.sections.TagSearchSection; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.ViewPartWithSections; + +import com.google.common.collect.ImmutableSet; + +/** + * @author Kris De Volder + */ +public class BootDashTreeView extends ViewPartWithSections implements ITabbedPropertySheetPageContributor, ISelectionProvider { + + /** + * The ID of the view as specified by the extension. + */ + public static final String ID = "org.springframework.ide.eclipse.boot.dash.views.BootDashView"; + + /** + * Adds scroll support to the whole view. You probably want to disable this + * if view is broken into pieces that have their own scrollbars + */ + private static final boolean ENABLE_SCROLLING = false; + + private BootDashViewModel model = BootDashActivator.getDefault().getModel(); + + // private Action refreshAction; + // private Action doubleClickAction; + + private BootDashActions actions; + + private MultiSelection selection = null; // lazy init + + private List selectionListeners = new ArrayList<>(); + + private BootDashUnifiedTreeSection treeSection; + + /* + * The content provider class is responsible for providing objects to the + * view. It can wrap existing objects in adapters or simply return objects + * as-is. These objects may be sensitive to the current input of the view, + * or ignore it and always show the same content (like Task List, for + * example). + */ + + /** + * The constructor. + */ + public BootDashTreeView() { + super(ENABLE_SCROLLING); + } + + @Override + public void dispose() { + super.dispose(); + if (actions != null) { + actions.dispose(); + } + } + + /** + * This is a callback that will allow us to create the viewer and initialize + * it. + */ + public void createPartControl(Composite parent) { + super.createPartControl(parent); + + getSite().setSelectionProvider(this); + getSite().registerContextMenu(treeSection.getMenuMgr(), this); + + // Create the help context id for the viewer's control + // PlatformUI.getWorkbench().getHelpSystem().setHelp(tv.getControl(), + // "org.springframework.ide.eclipse.boot.dash.viewer"); + actions = new BootDashActions(model, getRawSelection().filter(BootDashElement.class), context(), LiveProcessCommandsExecutor.getDefault()); + // hookContextMenu(); + // hookDoubleClickAction(); + contributeToActionBars(); + } + + private SimpleDIContext context() { + return model.getContext().injections; + } + + public synchronized MultiSelection getRawSelection() { + if (this.selection == null) { + MultiSelection selection = MultiSelection.empty(Object.class); + for (IPageSection section : getSections()) { + if (section instanceof MultiSelectionSource) { + MultiSelectionSource source = (MultiSelectionSource) section; + MultiSelection subSelection = source.getSelection().cast(Object.class); + selection = MultiSelection.union(selection, subSelection); + } + } + this.selection = selection; + selection.getElements().addListener(new ValueListener>() { + @Override + public void gotValue(LiveExpression> exp, ImmutableSet value) { + ISelection selection = getSelection(); + for (ISelectionChangedListener selectionListener : selectionListeners) { + selectionListener.selectionChanged(new SelectionChangedEvent(BootDashTreeView.this, selection)); + } + } + }); + } + return this.selection; + } + + public List getSelectedElements() { + ArrayList elements = new ArrayList<>(); + for (Object e : getRawSelection().getValue()) { + if (e instanceof BootDashElement) { + elements.add((BootDashElement) e); + } + } + return Collections.unmodifiableList(elements); + } + + private void contributeToActionBars() { + IActionBars bars = getViewSite().getActionBars(); + fillLocalPullDown(bars.getMenuManager()); + fillLocalToolBar(bars.getToolBarManager()); + } + + /** + * Fills the pull-down menu for this view (accessible from the toolbar) + */ + private void fillLocalPullDown(IMenuManager manager) { + for (RunStateAction a : actions.getRunStateActions()) { + manager.add(a); + //'addVisible' would be nice but doesm't work here. Probaly because the pulldown is only + // populated once rather than every time it shows: + // BootDashUnifiedTreeSection.addVisible(manager, a); + } + manager.add(actions.getOpenBrowserAction()); + manager.add(actions.getOpenNgrokAdminUi()); + manager.add(actions.getOpenConsoleAction()); + manager.add(actions.getLinkWithConsoleAction()); + manager.add(actions.getOpenInPackageExplorerAction()); + manager.add(actions.getOpenConfigAction()); + manager.add(actions.getShowPropertiesViewAction()); + MenuUtil.addDynamicSubmenu(manager, actions.getLiveDataConnectionManagement()); + + manager.add(new Separator()); + manager.add(actions.getExposeRunAppAction()); + manager.add(actions.getExposeDebugAppAction()); + + manager.add(new Separator()); + addAddRunTargetMenuActions(manager); + + manager.add(new Separator()); + //manager.add(actions.getToggleFiltersDialogAction()); + for (ToggleFilterAction a : actions.getToggleFilterActions()) { + manager.add(a); + } + + manager.add(new Separator()); + manager.add(actions.getOpenFilterPreferencesAction()); + + // manager.add(refreshAction); + // manager.add(new Separator()); + // manager.add(action2); + } + + private void fillLocalToolBar(IToolBarManager manager) { + for (RunStateAction a : actions.getRunStateActions()) { + if (a.showInToolbar()) { + manager.add(a); + } + } + manager.add(actions.getOpenBrowserAction()); + manager.add(actions.getOpenConsoleAction()); + manager.add(actions.getLinkWithConsoleAction()); + manager.add(actions.getOpenConfigAction()); + manager.add(actions.getShowPropertiesViewAction()); + MenuUtil.addDynamicSubmenu(manager, actions.getLiveDataConnectionManagement()); + manager.add(actions.getToggleFiltersDialogAction()); + +// This ought to work, but it doesn't. +// manager.add(createAddRunTargetMenuManager()); +// Must write specific code to create toolbar pull-down button / menu: + createAddRunTargetPulldown(manager); + // manager.add(refreshAction); + // manager.add(action2); + } + + private void addAddRunTargetMenuActions(IMenuManager manager) { + if (actions.getAddRunTargetActions().length==1) { + //Special case. Creationg a pulldown for just one item isn't very logical. + AddRunTargetAction action = actions.getAddRunTargetActions()[0]; + manager.add(action); + } else { + MenuManager menu = createAddRunTargetMenuManager(); + manager.add(menu); + } + + } + + private MenuManager createAddRunTargetMenuManager() { + final MenuManager menu = new MenuManager("Add Run Target...", BootDashActivator.getImageDescriptor("icons/add_target.png"), null); + for (AddRunTargetAction a : actions.getAddRunTargetActions()) { + menu.add(a); + } + return menu; + } + + + public void createAddRunTargetPulldown(IToolBarManager toolbar) { + if (actions.getAddRunTargetActions().length==1) { + //Special case. Creationg a pulldown for just one item isn't very logical. + AddRunTargetAction action = actions.getAddRunTargetActions()[0]; + toolbar.add(action); + } else { + Action dropdownAction=new Action("Create Target",SWT.DROP_DOWN){}; + dropdownAction.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/add_target.png")); + dropdownAction.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/add_target_disabled.png")); + dropdownAction.setMenuCreator(new IMenuCreator() { + Menu theMenu; + + @Override + public Menu getMenu(Menu parent) { + return null; + } + + @Override + public Menu getMenu(Control parent) { + if (theMenu==null) { + final MenuManager menu = createAddRunTargetMenuManager(); + theMenu = menu.createContextMenu(parent); + theMenu.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + menu.dispose(); + } + }); + } + return theMenu; + } + + @Override + public void dispose() { + } + }); + + toolbar.add(new ToolbarPulldownContributionItem(dropdownAction)); + } + } + + /** + * Passing the focus request to the viewer's control. + */ + public void setFocus() { + if (page != null) { + page.setFocus(); + } + } + + public UserInteractions ui() { + return context().getBean(UserInteractions.class); + } + + public BootDashActions getActions() { + return actions; + } + + @Override + public Shell getShell() { + return getSite().getShell(); + } + + @Override + protected List createSections() throws CoreException { + List sections = new ArrayList<>(); + sections.add(new TagSearchSection(BootDashTreeView.this, model.getFilterBox().getText(), model)); + sections.add(treeSection = new BootDashUnifiedTreeSection(this, model, context())); + + return sections; + } + + @Override + public String getContributorId() { + return "org.springframework.ide.eclipse.boot.dash.propertyContributor"; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Object getAdapter(Class adapter) { + if (adapter == IPropertySheetPage.class) { + return new TabbedPropertySheetPage(this); + } + return super.getAdapter(adapter); + } + + @Override + public void addSelectionChangedListener(ISelectionChangedListener listener) { + selectionListeners.add(listener); + } + + @Override + public ISelection getSelection() { + return new StructuredSelection(getRawSelection().getValue().toArray()); + } + + @Override + public void removeSelectionChangedListener(ISelectionChangedListener listener) { + selectionListeners.remove(listener); + } + + @Override + public void setSelection(ISelection selection) { + //This method isn't implemented. Probably this is okay, nobody needs to set our selection. + // If the need arises to do this in the future, then the 'setSelection' needs to be distributed to + // our subsections so each subsection can select whichever elements in the selection apply to them. + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/CustmomizeTargetLabelAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/CustmomizeTargetLabelAction.java new file mode 100644 index 000000000..fca66732e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/CustmomizeTargetLabelAction.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.dialogs.EditTemplateDialogModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +public class CustmomizeTargetLabelAction extends AbstractBootDashModelAction { + + protected CustmomizeTargetLabelAction(LiveExpression section, SimpleDIContext context) { + super(section, context); + setText("Customize Label..."); + } + + @Override + public void updateEnablement() { + this.setEnabled(isApplicable(sectionSelection.getValue())); + } + + public void updateVisibility() { + this.setVisible(isApplicable(sectionSelection.getValue())); + } + + private boolean isApplicable(BootDashModel section) { + if (section!=null) { + PropertyStoreApi props = section.getRunTarget().getType().getPersistentProperties(); + //Not all target types provide persistent properties yet. This feature only works on + // those target types that do. + return props!=null; + } + return false; + } + + @Override + public void run() { + final BootDashModel section = sectionSelection.getValue(); + if (isApplicable(section)) { + EditTemplateDialogModel model = CustomizeTargetLabelDialogModel.create(section); + ui().openEditTemplateDialog(model); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/CustomizeTargetLabelDialogModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/CustomizeTargetLabelDialogModel.java new file mode 100644 index 000000000..040552635 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/CustomizeTargetLabelDialogModel.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2016 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.dialogs.EditTemplateDialogModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; + +/** + * Model of the dialog opened by {@link CustmomizeTargetLabelAction}. Has all the functionality + * of the dialog except for the widgets themselves. + * + * @author Kris De Volder + */ +public final class CustomizeTargetLabelDialogModel { + + //Note: there is no representation in this model of the 'cancel' button that exists in the + // real dialog. When cancel is clicked basically... nothing happens and the dialog just closes, + // there is no call to the model and this is fine as there is literally nothing for the model to + // do. + + //Why not a 'real' class rather than this method that creates an anonymous class? + // Because the real class has troubles initializing itself because of the call to 'getDefaultValue' + // in the super constructor (it is called before the 'type' field is initialized! + //This static method is almost functionally equivalent but doesn't have that problem because + // the local variables exist before the class instance is constructed. + public static EditTemplateDialogModel create(final BootDashModel section ) { + final RunTargetType type = section.getRunTarget().getType(); + return new EditTemplateDialogModel() { + + { + template.setValue(section.getNameTemplate()); + } + + @Override + public String getTitle() { + String type = section.getRunTarget().getType().getName(); + return "Customize Labels for "+type+" Target(s)"; + } + + @Override + public void performOk() throws Exception { + if (applyToAll.getValue()) { + section.getRunTarget().getType().setNameTemplate(template.getValue()); + //To *really* apply the template to *all* targets of a given type, we must make sure + // that the targets do not override the value individually: + for (BootDashModel model : section.getViewModel().getSectionModels().getValue()) { + if (model.getRunTarget().getType().equals(type)) { + model.setNameTemplate(null); + model.notifyModelStateChanged(); + } + } + } else { + section.setNameTemplate(template.getValue()); + section.notifyModelStateChanged(); + } + } + + @Override + public String getHelpText() { + return type.getTemplateHelpText(); + } + + @Override + public String getDefaultValue() { + return type.getDefaultNameTemplate(); + } + + @Override + public String getApplyToAllLabel() { + return "Apply to all "+type.getName()+" targets"; + } + + @Override + public boolean getApplyToAllDefault() { + //'apply to all' is enabled by default, unless there is at least one applicable model which already + // has an individually customized label. + for (BootDashModel section : section.getViewModel().getSectionModels().getValue()) { + if ( + section.getRunTarget().getType().equals(type) && + section.hasCustomNameTemplate() + ) { + return false; + } + } + return true; + } + }; + } + + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DefaultUserInteractions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DefaultUserInteractions.java new file mode 100644 index 000000000..08b7e79c1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DefaultUserInteractions.java @@ -0,0 +1,330 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.ui.DebugUITools; +import org.eclipse.debug.ui.IDebugModelPresentation; +import org.eclipse.jdt.core.IType; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.IInputValidator; +import org.eclipse.jface.dialogs.InputDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.MessageDialogWithToggle; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.dialogs.ElementListSelectionDialog; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.dialogs.EditTemplateDialog; +import org.springframework.ide.eclipse.boot.dash.dialogs.EditTemplateDialogModel; +import org.springframework.ide.eclipse.boot.dash.dialogs.SelectRemoteEurekaDialog; +import org.springframework.ide.eclipse.boot.dash.dialogs.ToggleFiltersDialog; +import org.springframework.ide.eclipse.boot.dash.dialogs.ToggleFiltersDialogModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashTreeContentProvider; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.ui.UIContext; +import org.springsource.ide.eclipse.commons.ui.UiUtil; + +/** + * An implementation of 'UserInteractions' that uses real Dialogs, for use in + * 'production'. + * + * @author Kris De Volder + */ +public class DefaultUserInteractions implements UserInteractions { + + private final SimpleDIContext context; + + public DefaultUserInteractions(SimpleDIContext context) { + this.context = context; + context.assertDefinitionFor(UIContext.class); + } + + @Override + public ILaunchConfiguration chooseConfigurationDialog(final String dialogTitle, final String message, + final Collection configs) { + final LiveVariable chosen = new LiveVariable<>(); + getShell().getDisplay().syncExec(new Runnable() { + public void run() { + IDebugModelPresentation labelProvider = DebugUITools.newDebugModelPresentation(); + try { + ElementListSelectionDialog dialog = new ElementListSelectionDialog(getShell(), labelProvider); + dialog.setElements(configs.toArray()); + dialog.setTitle(dialogTitle); + dialog.setMessage(message); + dialog.setMultipleSelection(false); + int result = dialog.open(); + labelProvider.dispose(); + if (result == Window.OK) { + chosen.setValue((ILaunchConfiguration) dialog.getFirstResult()); + } + } finally { + labelProvider.dispose(); + } + } + }); + return chosen.getValue(); + } + + private Shell getShell() { + return context.getBean(UIContext.class).getShell(); + } + + @Override + public IType chooseMainType(final IType[] mainTypes, final String dialogTitle, final String message) { + if (mainTypes.length == 1) { + return mainTypes[0]; + } else if (mainTypes.length > 0) { + // Take care the UI interactions don't bork if called from non-ui + // thread. + final LiveVariable chosenType = new LiveVariable<>(); + getShell().getDisplay().syncExec(new Runnable() { + public void run() { + IDebugModelPresentation labelProvider = DebugUITools.newDebugModelPresentation(); + try { + ElementListSelectionDialog dialog = new ElementListSelectionDialog(getShell(), labelProvider); + dialog.setElements(mainTypes); + dialog.setTitle(dialogTitle); + dialog.setMessage(message); + dialog.setMultipleSelection(false); + int result = dialog.open(); + labelProvider.dispose(); + if (result == Window.OK) { + chosenType.setValue((IType) dialog.getFirstResult()); + } + } finally { + labelProvider.dispose(); + } + } + }); + return chosenType.getValue(); + } + return null; + } + + @Override + public void errorPopup(final String title, final String message) { + getShell().getDisplay().asyncExec(new Runnable() { + public void run() { + MessageDialog.openError(getShell(), title, message); + } + }); + } + + @Override + public void openLaunchConfigurationDialogOnGroup(final ILaunchConfiguration conf, final String launchGroup) { + getShell().getDisplay().asyncExec(new Runnable() { + public void run() { + IStructuredSelection selection = new StructuredSelection(new Object[] { conf }); + DebugUITools.openLaunchConfigurationDialogOnGroup(getShell(), selection, launchGroup); + } + }); + } + + @Override + public void openUrl(final String url) { + getShell().getDisplay().asyncExec(new Runnable() { + public void run() { + if (url != null) { + UiUtil.openUrl(url); + } + } + }); + } + + @Override + public boolean confirmOperation(final String title, final String message) { + final boolean[] confirm = { false }; + getShell().getDisplay().syncExec(new Runnable() { + public void run() { + confirm[0] = MessageDialog.openConfirm(getShell(), title, message); + } + }); + return confirm[0]; + } + + @Override + public void openDialog(final ToggleFiltersDialogModel model) { + final Shell shell = getShell(); + shell.getDisplay().syncExec(new Runnable() { + public void run() { + ToggleFiltersDialog dlg = new ToggleFiltersDialog("Select Filters", model, shell); + dlg.open(); + } + }); + } + + @Override + public String chooseFile(String title, String file) { + FileDialog fileDialog = new FileDialog(getShell()); + fileDialog.setText(title); + fileDialog.setFileName(file); + + String result = fileDialog.open(); + return result; + } + + @Override + public String selectRemoteEureka(BootDashViewModel model, String title, String message, String initialValue, IInputValidator validator) { + SelectRemoteEurekaDialog dialog = new SelectRemoteEurekaDialog(getShell(), new BootDashTreeContentProvider()); + dialog.setInput(model); + + dialog.setTitle("Select Eureka instance"); + dialog.setMessage("Select the Eureka instance this local app should be registered with"); + int open = dialog.open(); + if (open == Window.OK) { + String result = dialog.getSelectedEurekaURL(); + return result; + } + return null; + } + + @Override + public boolean yesNoWithToggle(final String propertyKey, final String title, final String message, final String toggleMessage) { + final String ANSWER = propertyKey+".answer"; + final String TOGGLE = propertyKey+".toggle"; + final IPreferenceStore store = getPreferencesStore(); + store.setDefault(ANSWER, true); + boolean toggleState = store.getBoolean(TOGGLE); + boolean answer = store.getBoolean(ANSWER); + if (toggleState) { + return answer; + } + final boolean[] dialog = new boolean[2]; + getShell().getDisplay().syncExec(new Runnable() { + @Override + public void run() { + MessageDialogWithToggle result = MessageDialogWithToggle.openYesNoQuestion(getShell(), title , message, toggleMessage, false, null, null); + dialog[0] = result.getReturnCode()==IDialogConstants.YES_ID; + dialog[1] = result.getToggleState(); + } + }); + store.setValue(TOGGLE, dialog[1]); + store.setValue(ANSWER, dialog[0]); + return dialog[0]; + } + + @Override + public boolean confirmWithToggle(final String propertyKey, final String title, final String message, final String toggleMessage) { + final IPreferenceStore store = getPreferencesStore(); + boolean toggleState = store.getBoolean(propertyKey); + if (toggleState) { + return true; + } + final boolean[] dialog = new boolean[2]; + getShell().getDisplay().syncExec(new Runnable() { + @Override + public void run() { + MessageDialogWithToggle result = MessageDialogWithToggle.openOkCancelConfirm(getShell(), title , message, toggleMessage, false, null, null); + dialog[0] = result.getReturnCode()==IDialogConstants.OK_ID; + dialog[1] = result.getToggleState(); + } + }); + store.setValue(propertyKey, dialog[0] && dialog[1]); + return dialog[0]; + } + + protected IPreferenceStore getPreferencesStore() { + return BootDashActivator.getDefault().getPreferenceStore(); + } + + @Override + public void openEditTemplateDialog(final EditTemplateDialogModel model) { + getShell().getDisplay().syncExec(new Runnable() { + public void run() { + new EditTemplateDialog(model, getShell()).open(); + } + }); + } + + @Override + public int confirmOperation(String title, String message, String[] buttonLabels, int defaultButtonIndex) { + AtomicInteger answer = new AtomicInteger(); + getShell().getDisplay().syncExec(() -> { + answer.set(new MessageDialog(getShell(), title, null, message, + MessageDialog.QUESTION, buttonLabels, defaultButtonIndex).open() + ); + }); + return answer.get(); + } + + @Override + public void warningPopup(String title, String message) { + getShell().getDisplay().asyncExec(new Runnable() { + public void run() { + MessageDialog.openWarning(getShell(), title, message); + } + }); + } + + @Override + public T chooseElement(final String dialogTitle, final String message, + final List elements, Function labelFun) { + try (LiveVariable chosen = new LiveVariable<>()) { + getShell().getDisplay().syncExec(new Runnable() { + @SuppressWarnings("unchecked") + public void run() { + ILabelProvider labelProvider = new LabelProvider() { + public String getText(Object element) { + return labelFun.apply((T) element); + } + }; + try { + ElementListSelectionDialog dialog = new ElementListSelectionDialog(getShell(), labelProvider); + dialog.setElements(elements.toArray()); + dialog.setTitle(dialogTitle); + dialog.setMessage(message); + dialog.setMultipleSelection(false); + int result = dialog.open(); + labelProvider.dispose(); + if (result == Window.OK) { + chosen.setValue((T) dialog.getFirstResult()); + } + } finally { + labelProvider.dispose(); + } + } + }); + return chosen.getValue(); + } + } + + @Override + public String inputDialog(String dialogTitle, String prompt, String defaultValue) { + AtomicReference result = new AtomicReference<>(); + getShell().getDisplay().syncExec(new Runnable() { + public void run() { + InputDialog dlg = new InputDialog(getShell(), dialogTitle, prompt, defaultValue, null); + int code = dlg.open(); + if (code==IDialogConstants.OK_ID) { + result.set(dlg.getValue()); + } + } + }); + return result.get(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DeleteElementsAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DeleteElementsAction.java new file mode 100644 index 000000000..f427f581b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DeleteElementsAction.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import java.util.Collection; +import java.util.Map.Entry; + +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.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.api.RunTargetType; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.DeletionCapabableModel; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +public class DeleteElementsAction extends AbstractBootDashElementsAction { + + private Class targetTypeClass; + + public DeleteElementsAction(BootDashActions actions, Class targetType, MultiSelection selection, SimpleDIContext context) { + super(new Params(actions) + .setSelection(selection) + .setContext(context) + ); + this.targetTypeClass = targetType; + context.assertDefinitionFor(UserInteractions.class); + this.setText("Delete Elements"); + this.setToolTipText("Delete the selected elements."); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/delete_app.png")); + this.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/delete_app_disabled.png")); + } + + @Override + public void run() { + //Deletes are implemented per BootDashModel. So sort selection into bins per model. + Multimap sortingBins = HashMultimap.create(); + for (BootDashElement e : getSelectedElements()) { + BootDashModel model = e.getBootDashModel(); + //We are only capable of removing elements from a DeleteCapabableModel. + if (model instanceof DeletionCapabableModel) { + sortingBins.put(model, e); + } + } + //Now delete elements from corresponding models. + for (final Entry> workitem : sortingBins.asMap().entrySet()) { + BootDashModel model = workitem.getKey(); + final DeletionCapabableModel modifiable = (DeletionCapabableModel)model; //cast is safe. Only DeleteCapabableModel are added to sortingBins + String confirmationMsg = modifiable.getDeletionConfirmationMessage(workitem.getValue()); + if (confirmationMsg==null || ui().confirmOperation("Deleting Elements", confirmationMsg)) { + Job job = new Job("Deleting Elements from " + model.getRunTarget().getName()) { + @Override + protected IStatus run(IProgressMonitor monitor) { + modifiable.delete(workitem.getValue(), ui()); + return Status.OK_STATUS; + } + + }; + job.schedule(); + } + } + } + + @Override + public void updateEnablement() { + this.setEnabled(shouldEnableFor(getSelectedElements())); + } + + @Override + public void updateVisibility() { + this.setVisible(shouldShowFor(getSelectedElements())); + } + + protected boolean shouldShowFor(Collection selectedElements) { + //Only visible if all selected elements belong to the correct runtarget type + if (selectedElements.isEmpty()) { + return false; + } + for (BootDashElement e : selectedElements) { + if (!isCorrectTargetType(e.getBootDashModel())) { + return false; + } + } + return true; + } + + protected boolean shouldEnableFor(Collection selectedElements) { + if (selectedElements.isEmpty()) { + //If no elements are selected, then action would do nothing, so disable it. + return false; + } + //All selected elements must be deletable, then this action is enabled. + for (BootDashElement bde : selectedElements) { + if (!canDelete(bde)) { + return false; + } + } + return true; + } + + private boolean canDelete(BootDashElement bde) { + BootDashModel model = bde.getBootDashModel(); + return isCorrectTargetType(model) && model instanceof DeletionCapabableModel && + ((DeletionCapabableModel)model).canDelete(bde); + } + + private boolean isCorrectTargetType(BootDashModel model) { + return model!=null && targetTypeClass.isAssignableFrom(model.getRunTarget().getType().getClass()); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DuplicateConfigAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DuplicateConfigAction.java new file mode 100644 index 000000000..4783b7000 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DuplicateConfigAction.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2016, 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.Duplicatable; + +/** + * @author Kris De Volder + */ +public class DuplicateConfigAction extends AbstractBootDashElementsAction { + + public DuplicateConfigAction(Params params) { + super(params); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/copy.png")); + this.setText("Duplicate Config"); + this.setToolTipText("Make a copy of this element's LaunchConfiguration"); + } + + @Override + public void updateEnablement() { + BootDashElement element = getSingleSelectedElement(); + setEnabled(element != null && element instanceof Duplicatable && ((Duplicatable) element).canDuplicate()); + } + + @Override + public void updateVisibility() { + setVisible(getSingleSelectedElement() instanceof Duplicatable); + } + + @Override + public void run() { + BootDashElement _e = getSingleSelectedElement(); + if (_e instanceof Duplicatable) { + Duplicatable e = (Duplicatable) _e; + if (e.canDuplicate()) { + e.duplicate(ui()); + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DynamicRunTargetSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DynamicRunTargetSection.java new file mode 100644 index 000000000..e40d8918f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/DynamicRunTargetSection.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views; + +import java.util.Set; + +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.views.sections.DynamicCompositeSection; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; + +public class DynamicRunTargetSection extends DynamicCompositeSection { + + public DynamicRunTargetSection(IPageWithSections owner, LiveExpression> models, + SectionFactory factory) { + super(owner, models, factory, BootDashElement.class); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/EnableRemoteDevtoolsAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/EnableRemoteDevtoolsAction.java new file mode 100644 index 000000000..4a64d0c62 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/EnableRemoteDevtoolsAction.java @@ -0,0 +1,149 @@ +package org.springframework.ide.eclipse.boot.dash.views; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.PlatformUI; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springframework.ide.eclipse.boot.core.ISpringBootProject; +import org.springframework.ide.eclipse.boot.core.SpringBootCore; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.SystemPropertySupport; +import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsUtil; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElement; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * Action for starting/restarting Remove DevTools Client application + * + * @author Alex Boyko + * + */ +public class EnableRemoteDevtoolsAction extends AbstractBootDashElementsAction { + + private ElementStateListener stateListener; + private boolean enable; + + /** + * For testing code to be able to observe / synchronize with the end of a executed action. + */ + public CompletableFuture lastOperation; + + public EnableRemoteDevtoolsAction(Params params) { + super(params); + this.setText("Enable Remote DevTools Server"); + this.setToolTipText("Enables server-side Remote DevTools support for remote application."); + URL url = FileLocator.find(Platform.getBundle("org.springframework.ide.eclipse.boot"), new Path("resources/icons/boot-devtools-icon.png"), null); + if (url != null) { + this.setImageDescriptor(ImageDescriptor.createFromURL(url)); + } + if (model != null) { + model.addElementStateListener(stateListener = new ElementStateListener() { + public void stateChanged(BootDashElement e) { + if (getSelectedElements().contains(e) && !PlatformUI.getWorkbench().isClosing()) { + PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { + public void run() { + updateEnablement(); + } + }); + } + } + }); + } + } + + @Override + public void updateVisibility() { + boolean visible = false; + if (getSelectedElements().size()==1) { + visible = true; + for (BootDashElement e : getSelectedElements()) { + if (!visibleForElement(e)) { + visible = false; + break; + } + } + } + setVisible(visible); + } + + private boolean visibleForElement(BootDashElement e) { + if (e instanceof GenericRemoteAppElement) { + App data = ((GenericRemoteAppElement) e).getAppData(); + return data instanceof SystemPropertySupport; + } + return false; + } + + @Override + public void updateEnablement() { + boolean enable = false; + if (!getSelectedElements().isEmpty()) { + enable = true; + for (BootDashElement e : getSelectedElements()) { + if (!enableForElement(e)) { + enable = false; + break; + } + } + } + this.setEnabled(enable); + } + + private boolean enableForElement(BootDashElement bde) { + try { + IProject project = bde.getProject(); + if (visibleForElement(bde) && project!=null) { + if (BootPropertyTester.fastHasDevTools(bde.getProject())) { + App data = ((GenericRemoteAppElement)bde).getAppData(); + if (data instanceof SystemPropertySupport) { + String secret = ((SystemPropertySupport)data).getSystemProperty(DevtoolsUtil.REMOTE_SECRET_PROP); + this.enable = secret==null; + if (enable) { + this.setText("Enable Remote DevTools Server"); + this.setToolTipText("Enables server-side Remote DevTools support for remote application."); + } else { + this.setText("Disable Remote DevTools Server"); + this.setToolTipText("Disables server-side Remote DevTools support for remote application."); + } + return true; + } + } + } + } catch (Exception e) { + Log.log(e); + } + return false; + } + + @Override + public void run() { + List> futures = new ArrayList<>(); + for (BootDashElement _e : getSelectedElements()) { + if (_e instanceof GenericRemoteAppElement && _e.getProject() != null) { + GenericRemoteAppElement e = (GenericRemoteAppElement) _e; + futures.add(e.enableDevtools(enable)); + } + } + lastOperation = CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])); + } + + @Override + public void dispose() { + if (model != null && stateListener != null) { + model.removeElementStateListener(stateListener); + stateListener = null; + } + super.dispose(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ExposeAppAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ExposeAppAction.java new file mode 100644 index 000000000..8a6076253 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ExposeAppAction.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.views; + +import java.util.Collection; + +import org.eclipse.core.runtime.Assert; +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.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.dash.model.AbstractLaunchConfigurationsDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.LocalCloudServiceDashElement; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKClient; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKInstallManager; + +/** + * @author Martin Lippert + */ +public class ExposeAppAction extends RunStateAction { + + private NGROKInstallManager ngrokManager; + + public ExposeAppAction(Params params, final RunState goalState, final NGROKInstallManager ngrokManager) { + super(params, goalState); + + Assert.isLegal(goalState == RunState.RUNNING || goalState == RunState.DEBUGGING); + + this.ngrokManager = ngrokManager; + + model.addElementStateListener(new ElementStateListener() { + @Override + public void stateChanged(BootDashElement e) { + if (e instanceof AbstractLaunchConfigurationsDashElement && RunState.INACTIVE.equals(e.getRunState())) { + try { + AbstractLaunchConfigurationsDashElement localDashProject = (AbstractLaunchConfigurationsDashElement) e; + localDashProject.shutdownExpose(); + } catch (Exception ex) { + ui().errorPopup("error shutting down tunnel", "error shutting down tunnel"); + } + } + } + }); + } + + @Override + protected boolean appliesToElement(BootDashElement bootDashElement) { + return bootDashElement.getTarget().getType().equals(RunTargetTypes.LOCAL) && !(bootDashElement instanceof LocalCloudServiceDashElement); + } + + @Override + protected Job createJob() { + final Collection selecteds = getSelectedElements(); + if (!selecteds.isEmpty()) { + boolean riskAccepted = ui().confirmWithToggle("ngrok.tunnel.warning.state", "Really Expose local service on public internet?", + "The ngrok tunnel uses a third-party server to pass all data between your local app and its clients over a public internet connection.\n\n" + + "Do you really want to do this?", + null); + if (riskAccepted) { + String ngrokInstall = this.ngrokManager.getDefaultInstall(); + if (ngrokInstall == null) { + ngrokInstall = ui().chooseFile("ngrok installation", null); + if (ngrokInstall != null) { + this.ngrokManager.addInstall(ngrokInstall); + this.ngrokManager.setDefaultInstall(ngrokInstall); + this.ngrokManager.save(); + } + } + + if (ngrokInstall != null) { + final NGROKClient ngrokClient = new NGROKClient(ngrokInstall); + + final String eurekaInstance = getEurekaInstance(); + if (eurekaInstance != null) { + return new Job("Restarting and Exposing " + selecteds.size() + " Dash Elements") { + @Override + public IStatus run(IProgressMonitor monitor) { + monitor.beginTask("Restart and Expose Boot Dash Elements", selecteds.size()); + try { + for (BootDashElement el : selecteds) { + if (el instanceof AbstractLaunchConfigurationsDashElement) { + monitor.subTask("Restarting: " + el.getName()); + try { + AbstractLaunchConfigurationsDashElement localDashProject = (AbstractLaunchConfigurationsDashElement) el; + localDashProject.restartAndExpose(getGoalState(), ngrokClient, eurekaInstance, ui()); + } catch (Exception e) { + return BootActivator.createErrorStatus(e); + } + } + monitor.worked(1); + } + return Status.OK_STATUS; + } finally { + monitor.done(); + } + } + }; + } + } + } + } + return null; + } + + private String getEurekaInstance() { + String eurekaInstance = ui().selectRemoteEureka(model, "Eureka URL", "please enter the full URL of the Eureka instance you would like to use", "", null); + + if (eurekaInstance != null) { + if (eurekaInstance.endsWith("/eureka") || eurekaInstance.endsWith("/eureka/")) { + return eurekaInstance; + } + else if (eurekaInstance.endsWith("/")) { + eurekaInstance = eurekaInstance + "eureka/"; + } + else { + eurekaInstance = eurekaInstance + "/eureka/"; + } + } + + return eurekaInstance; + } + + + + @Override + public void updateVisibility() { + boolean visible = !getSelectedElements().isEmpty(); + for (BootDashElement e : getSelectedElements()) { + if (!RunTargetTypes.LOCAL.equals(e.getTarget().getType())) { + visible = false; + break; + } + } + setVisible(visible); + } + + @Override + public void dispose() { + super.dispose(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ImageDecorator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ImageDecorator.java new file mode 100644 index 000000000..516dfd311 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ImageDecorator.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.DecorationOverlayIcon; +import org.eclipse.jface.viewers.IDecoration; +import org.eclipse.swt.graphics.Image; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; +import org.springsource.ide.eclipse.commons.ui.ImageDescriptorRegistry; + +/** + * Responsible for creating optionally decorated images from + * image descriptors. Keeps track of the created images and + * can dispose them when no longer used. + * + * @author Kris De Volder + */ +public class ImageDecorator implements Disposable { + + private static final boolean DEBUG = false; //(""+Platform.getLocation()).contains("kdvolder"); + + /** + * Keeps track of undecorated images. + */ + private ImageDescriptorRegistry images; + + /** + * Keeps track of decorated images. + */ + private Map decoratedImages; + + @Override + public void dispose() { + if (images!=null) { + images.dispose(); + images = null; + } + if (decoratedImages!=null) { + for (Image i : decoratedImages.values()) { + i.dispose(); + } + decoratedImages = null; + } + } + + public Image get(ImageDescriptor icon, ImageDescriptor decoration) { + if (decoration==null) { + return get(icon); + } else { + if (decoratedImages==null) { + decoratedImages = new HashMap<>(); + } + Object key = keyFor(icon, decoration); + Image existing = decoratedImages.get(key); + if (existing==null) { + debug("Decorating: "+icon + " with "+decoration); + Image baseImg = get(icon); + DecorationOverlayIcon overlayer = new DecorationOverlayIcon(baseImg, + decoration, IDecoration.BOTTOM_RIGHT); + decoratedImages.put(key, existing = overlayer.createImage()); + } + return existing; + } + } + + private Image get(Image baseImg, ImageDescriptor decoration) { + if (decoration==null) { + return baseImg; + } else { + if (decoratedImages==null) { + decoratedImages = new HashMap<>(); + } + Object key = keyFor(baseImg, decoration); + Image existing = decoratedImages.get(key); + if (existing==null) { + debug("Decorating: "+baseImg + " with "+decoration); + DecorationOverlayIcon overlayer = new DecorationOverlayIcon(baseImg, + decoration, IDecoration.TOP_RIGHT); + decoratedImages.put(key, existing = overlayer.createImage()); + } + return existing; + } + } + + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + /** + * Get plain, undecorated image. + */ + private Image get(ImageDescriptor desc) { + if (images==null) { + images = new ImageDescriptorRegistry(); + } + return images.get(desc); + } + + private Object keyFor(Object icon, ImageDescriptor decoration) { + return Arrays.asList(icon, decoration); + } + + public Image[] decorateImages(Image[] anim, ImageDescriptor decoration) { + Image[] decorated = new Image[anim.length]; + for (int i = 0; i < decorated.length; i++) { + decorated[i] = get(anim[i], decoration); + } + return decorated; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/LinkWithConsoleAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/LinkWithConsoleAction.java new file mode 100644 index 000000000..38559e1ec --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/LinkWithConsoleAction.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2019, 2020 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.views; + +import java.util.Collection; +import java.util.Iterator; + +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.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElement; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +import com.google.common.collect.ImmutableSet; + +public class LinkWithConsoleAction extends AbstractBootDashElementsAction { + + + private ValueListener> listener; + + public LinkWithConsoleAction(Params params) { + super(params); + this.setText("Link with Console"); + this.setToolTipText("Link with Console"); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/link_to_editor.png")); + + selection.getElements().addListener(listener = new ValueListener>() { + public void gotValue(LiveExpression> exp, ImmutableSet selecteds) { + linkToConsole(); + } + }); + } + + @Override + public void run() { + linkToConsole(); + } + + @Override + public void dispose() { + if(listener != null && selection != null) { + selection.getElements().removeListener(listener); + } + super.dispose(); + } + + protected void linkToConsole() { + if (LinkWithConsoleAction.this.isChecked()) { + showSelected(getSelectedElements()); + } + } + + protected void showSelected(Collection selected) { + + Job job = new Job("Opening console") { + + @Override + protected IStatus run(IProgressMonitor monitor) { + doShowConsoles(selected); + return Status.OK_STATUS; + } + + }; + job.schedule(); + } + + protected void doShowConsoles(Collection selectedElements) { + + if (selectedElements != null) { + + Iterator it = selectedElements.iterator(); + + // Show first element only for now + if (it.hasNext()) { + BootDashElement element = selectedElements.iterator().next(); + BootDashModel model = element.getBootDashModel(); + + try { + BootDashModelConsoleManager consoleManager = model.getElementConsoleManager(); + + // The difference between this and open console is that we do NOT want to show + // a pop-up dialogue when the error is related to the console not existing. We don't + // consider this an error condition if the console is not available + + if (consoleManager != null && consoleManager.safeGetOrCreateConsole(element) != null) { + consoleManager.showConsole(element); + } + + } catch (Exception e) { + ui().errorPopup("Open Console Failure", e.getMessage()); + } + } + } + } + + @Override + public void updateVisibility() { + this.setVisible(supportsConsole()); + } + + @Override + public void updateEnablement() { + this.setEnabled(supportsConsole()); + } + + protected boolean supportsConsole() { + BootDashElement element = getSingleSelectedElement(); + boolean supports = false; + if (element != null) { + if (element instanceof GenericRemoteAppElement) { + supports = ((GenericRemoteAppElement) element).canWriteToConsole(); + } else { + // Backward compatibility with CF. Console action is enabled when a single CF element + // is selected + supports = true; + } + } + return supports; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/LocalElementConsoleManager.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/LocalElementConsoleManager.java new file mode 100644 index 000000000..931c07d82 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/LocalElementConsoleManager.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.views; + +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.model.IProcess; +import org.eclipse.debug.internal.ui.views.console.ProcessConsole; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleManager; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.console.LogType; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.launch.util.BootLaunchUtils; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +import com.google.common.collect.ImmutableSet; + +@SuppressWarnings("restriction") +public class LocalElementConsoleManager extends BootDashModelConsoleManager { + + @Override + public void doWriteToConsole(App element, String message, LogType type) throws Exception { + // Ignore message and log type for now. + // For local apps, console content is generated by the Java process for + // the project. + } + + /** + * @return IConsole that is associated with the given process. Null if no + * console is found. + */ + protected IConsole getConsole(IProcess process, IConsole[] consoles) { + + for (IConsole console : consoles) { + + if (console instanceof ProcessConsole) { + IProcess consoleProcess = ((ProcessConsole) console).getProcess(); + + if (consoleProcess != null && consoleProcess.equals(process)) { + return console; + } + } + } + return null; + } + + @Override + public void terminateConsole(App element) throws Exception { + // Not supported + } + + @Override + public void showConsole(App element) throws Exception { + IConsoleManager manager = eclipseConsoleManager(); + + IConsole appConsole = getConsole(element); + + + if (appConsole != null) { + manager.showConsoleView(appConsole); + } else { + throw ExceptionUtil.coreException("Failed to open console for: " + element.getName() + + ". Either a process console may not exist or the application is not running."); + } + } + + private IConsole getConsole(App element) { + IConsoleManager manager = eclipseConsoleManager(); + IConsole[] activeConsoles = manager.getConsoles(); + if (activeConsoles != null && element instanceof BootDashElement) { + ImmutableSet launchConfs = ((BootDashElement)element).getLaunchConfigs(); + for (ILaunch launch : BootLaunchUtils.getLaunches(launchConfs)) { + IProcess[] processes = launch.getProcesses(); + if (processes != null) { + for (IProcess process : processes) { + IConsole console = getConsole(process, activeConsoles); + if (console!=null) { + return console; + } + } + } + } + } + return null; + } + + protected IConsoleManager eclipseConsoleManager() { + return ConsolePlugin.getDefault().getConsoleManager(); + } + + @Override + public IConsole safeGetOrCreateConsole(App element) { + return getConsole(element); + } + + @Override + public void resetConsole(App element) { + // Not supported + } + + @Override + public void reconnect(App element) throws Exception { + // Not supported + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenConsoleAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenConsoleAction.java new file mode 100644 index 000000000..6bb5328e8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenConsoleAction.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * 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.views; + +import java.util.Collection; +import java.util.Iterator; + +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.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElement; + +public class OpenConsoleAction extends AbstractBootDashElementsAction { + + public OpenConsoleAction(Params params) { + super(params); + this.setText("Open Console"); + this.setToolTipText("Open Console"); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/open_console.png")); + this.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/open_console_disabled.png")); + } + + @Override + public void run() { + final Collection selecteds = getSelectedElements(); + showSelected(ui(), selecteds); + } + + protected void showSelected(UserInteractions ui, Collection selected) { + + Job job = new Job("Opening console") { + + @Override + protected IStatus run(IProgressMonitor monitor) { + doShowConsoles(selected); + return Status.OK_STATUS; + } + + }; + job.schedule(); + } + + protected void doShowConsoles(Collection selectedElements) { + + if (selectedElements != null) { + + Iterator it = selectedElements.iterator(); + + // Show first element only for now + if (it.hasNext()) { + BootDashElement element = selectedElements.iterator().next(); + BootDashModel model = element.getBootDashModel(); + try { + BootDashModelConsoleManager consoleManager = model.getElementConsoleManager(); + if (consoleManager != null) { + consoleManager.showConsole(element); + } + + } catch (Exception e) { + ui().errorPopup("Open Console Failure", e.getMessage()); + } + } + } + } + + @Override + public void updateVisibility() { + this.setVisible(supportsConsole()); + } + + @Override + public void updateEnablement() { + this.setEnabled(supportsConsole()); + } + + protected boolean supportsConsole() { + BootDashElement element = getSingleSelectedElement(); + boolean supports = false; + if (element != null) { + if (element instanceof GenericRemoteAppElement) { + supports = ((GenericRemoteAppElement) element).canWriteToConsole(); + } else { + // Backward compatibility with CF. Console action is enabled when a single CF element + // is selected + supports = true; + } + } + return supports; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenFilterPreferencesAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenFilterPreferencesAction.java new file mode 100644 index 000000000..c0215c507 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenFilterPreferencesAction.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2016 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.views; + +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.dialogs.PreferencesUtil; +import org.springframework.ide.eclipse.boot.core.BootPreferences; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +/** + * Action opens Preferences dialog at Spring -> Boot page to allow Spring Boot + * projects filtering + * + * @author Alex Boyko + * + */ +public class OpenFilterPreferencesAction extends AbstractBootDashAction { + + protected OpenFilterPreferencesAction(SimpleDIContext context) { + super(context, IAction.AS_PUSH_BUTTON); + setText("Boot Projects Filters Preferences..."); + setToolTipText("Open Preferences for Spring Boot projects filters"); + } + + @Override + public void run() { + PreferencesUtil.createPreferenceDialogOn(null, BootPreferences.BOOT_PREFERENCE_PAGE_ID, null, null).open(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenInBrowserAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenInBrowserAction.java new file mode 100644 index 000000000..11a8bf841 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenInBrowserAction.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2015, 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; + +public class OpenInBrowserAction extends AbstractBootDashElementsAction { + + public OpenInBrowserAction(Params params) { + super(params); + this.setText("Open Web Browser"); + this.setToolTipText("Open a Web Browser on the default URL"); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/open_browser.png")); + this.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/open_browser_disabled.png")); + } + + @Override + public void updateEnablement() { + String url = getUrl(); + setEnabled(url!=null); + } + + private String getUrl() { + BootDashElement el = getSingleSelectedElement(); + if (el!=null) { + return el.getUrl(); + } + return null; + } + + @Override + public void run() { + String url = getUrl(); + if (url!=null) { + ui().openUrl(url); + } + } + + @Override + public void dispose() { + super.dispose(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenInPackageExplorer.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenInPackageExplorer.java new file mode 100644 index 000000000..1f7eb329a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenInPackageExplorer.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import java.util.Collection; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; + +@SuppressWarnings("restriction") +public class OpenInPackageExplorer extends AbstractBootDashElementsAction { + + public OpenInPackageExplorer(Params params) { + super(params); + this.setText("Open In Package Explorer"); + this.setToolTipText("Open In Package Explorer"); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/package.png")); + this.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/package.png")); + } + + @Override + public void updateEnablement() { + Collection selectedElements = getSelectedElements(); + if (selectedElements.size() != 1) { + this.setEnabled(false); + } + else { + BootDashElement selectedElement = selectedElements.iterator().next(); + IProject project = selectedElement.getProject(); + this.setEnabled(project != null); + } + } + + @Override + public void run() { + final Collection selected = getSelectedElements(); + BootDashElement selectedProject = selected.iterator().next(); + + IProject project = selectedProject.getProject(); + if (project != null) { + PackageExplorerPart view= PackageExplorerPart.openInActivePerspective(); + view.tryToReveal(project); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenLaunchConfigAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenLaunchConfigAction.java new file mode 100644 index 000000000..2ae7a22e0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenLaunchConfigAction.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) 2015, 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import java.util.Collection; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement; +import org.springframework.ide.eclipse.boot.dash.model.LaunchConfDashElement; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.LocalRunTargetType; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; + +/** + * @author Kris De Volder + */ +public class OpenLaunchConfigAction extends AbstractBootDashElementsAction { + + public OpenLaunchConfigAction(Params params) { + super(params); + this.setText("Open Config"); + this.setToolTipText("Open the launch configuration associated with the selected element, if one exists, or create one if it doesn't."); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/write_obj.png")); + this.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/write_obj_disabled.png")); + } + + @Override + public void run() { + Collection selecteds = getSelectedElements(); + for (BootDashElement bootDashElement : selecteds) { + bootDashElement.openConfig(ui()); + } + } + + @Override + public void updateEnablement() { + setEnabled(shouldEnable()); + } + + private boolean shouldEnable() { + BootDashElement element = getSingleSelectedElement(); + if (element instanceof BootProjectDashElement) { + BootProjectDashElement projectEl = (BootProjectDashElement) element; + ObservableSet confs = projectEl.getChildren(); + return confs.getValues().size()<=1; + } else if (element instanceof LaunchConfDashElement) { + return true; + } + return false; + } + + @Override + public void updateVisibility() { + setVisible(shouldShow()); + } + + private boolean shouldShow() { + //Only show if all selected elements are local elements + if (getSelectedElements().isEmpty()) { + return false; + } + for (BootDashElement e : getSelectedElements()) { + if (!isCorrectTargetType(e)) { + return false; + } + } + return true; + } + + private boolean isCorrectTargetType(BootDashElement e) { + return e.getTarget().getType() instanceof LocalRunTargetType; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenNgrokAdminUi.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenNgrokAdminUi.java new file mode 100644 index 000000000..1b7a36c5b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenNgrokAdminUi.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2016, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.LocalRunTarget; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKClient; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKLaunchTracker; + +import com.google.common.collect.ImmutableSet; + +public class OpenNgrokAdminUi extends AbstractBootDashElementsAction { + + public OpenNgrokAdminUi(Params params) { + super(params); + this.setText("Open Ngrok Admin UI"); + this.setToolTipText("Opens Ngrok Admin UI for this app in a Web Browser"); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/open_browser.png")); //TODO: different icon? + this.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/open_browser_disabled.png")); + } + + @Override + public void updateVisibility() { + // Visible if at least one local app is selected + this.setVisible(getSelectedElements().stream().anyMatch(this::isLocalApp)); + } + + @Override + public void updateEnablement() { + BootDashElement selected = getSingleSelectedElement(); + this.setEnabled(selected != null && getClient(selected) != null); + } + + private NGROKClient getClient(BootDashElement bde) { + if (isLocalApp(bde)) { + ImmutableSet launchConfigs = bde.getLaunchConfigs(); + if (launchConfigs.size() == 1) { + return NGROKLaunchTracker.get(launchConfigs.iterator().next().getName()); + } + } + return null; + } + + boolean isLocalApp(BootDashElement bde) { + return bde != null && bde.getTarget() instanceof LocalRunTarget; + } + + @Override + public void run() { + NGROKClient client = getClient(getSingleSelectedElement()); + if (client != null) { + String url = client.getURL(); + if (url != null) { + ui().openUrl(url); + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenToggleFiltersDialogAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenToggleFiltersDialogAction.java new file mode 100644 index 000000000..24f4e17e2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/OpenToggleFiltersDialogAction.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.dialogs.ToggleFiltersDialogModel; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +/** + * @author Kris De Volder + */ +public class OpenToggleFiltersDialogAction extends AbstractBootDashAction { + + /** + * Represents the filters in the view (i.e. the ones currently in effect when dlg opens). + */ + private ToggleFiltersModel viewModel; + + public OpenToggleFiltersDialogAction(ToggleFiltersModel model, MultiSelection selection, SimpleDIContext ui) { + super(ui); + this.viewModel = model; + setText("Filters..."); + setImageDescriptor(BootDashActivator.getImageDescriptor("icons/filter.png")); + setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/filter_disabled.png")); + } + + @Override + public void run() { + ToggleFiltersDialogModel dlg = new ToggleFiltersDialogModel(viewModel); + ui().openDialog(dlg); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RefreshRunTargetAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RefreshRunTargetAction.java new file mode 100644 index 000000000..ba45adb72 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RefreshRunTargetAction.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTarget; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +public class RefreshRunTargetAction extends AbstractBootDashModelAction { + + public RefreshRunTargetAction(LiveExpression section, SimpleDIContext ui) { + super(section, ui); + this.setText("Refresh"); + this.setToolTipText("Manually refresh contents of the section"); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/refresh.png")); + } + + @Override + public void run() { + BootDashModel model = sectionSelection.getValue(); + if (model!=null) { + model.refresh(ui()); + } + } + + @Override + public void updateEnablement() { + super.updateEnablement(); + BootDashModel model = sectionSelection.getValue(); + if (model!=null) { + RunTarget target = model.getRunTarget(); + if (target instanceof RemoteRunTarget) { + setEnabled(((RemoteRunTarget) target).isConnected()); + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RemoveRunTargetAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RemoveRunTargetAction.java new file mode 100644 index 000000000..17a454667 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RemoveRunTargetAction.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.views; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +public class RemoveRunTargetAction extends AbstractBootDashModelAction { + + private BootDashViewModel viewModel; + + public RemoveRunTargetAction(LiveExpression sectionSelection, BootDashViewModel viewModel, + SimpleDIContext ui) { + super(sectionSelection, ui); + + this.viewModel = viewModel; + this.setText("Remove Target"); + this.setToolTipText("Remove the connection to the target and its dashboard section."); + this.setImageDescriptor(BootDashActivator.getImageDescriptor("icons/remove_target.png")); + this.setDisabledImageDescriptor(BootDashActivator.getImageDescriptor("icons/remove_target_disabled.png")); + } + + + @Override + public void run() { + RunTarget target = getRunTargetToRemove(); + viewModel.removeTarget(target, ui()); + } + + @Override + public void updateEnablement() { + RunTarget runTargetToRemove = getRunTargetToRemove(); + this.setEnabled(runTargetToRemove!=null && runTargetToRemove.canRemove()); + } + + @Override + public void updateVisibility() { + RunTarget runTargetToRemove = getRunTargetToRemove(); + setVisible(runTargetToRemove != null && !RunTargetTypes.LOCAL.equals(runTargetToRemove.getType())); + } + + private RunTarget getRunTargetToRemove() { + BootDashModel section = sectionSelection.getValue(); + if (section!=null) { + return section.getRunTarget(); + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RequestMappingLabelProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RequestMappingLabelProvider.java new file mode 100644 index 000000000..9c658de2e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RequestMappingLabelProvider.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import org.eclipse.jface.viewers.StyledCellLabelProvider; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.jface.viewers.StyledString.Styler; +import org.eclipse.jface.viewers.ViewerCell; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +/** + * @author Kris De Volder + */ +public class RequestMappingLabelProvider extends StyledCellLabelProvider { + + private LiveExpression bde; + private Stylers stylers; + private RequestMappingsColumn column; + + public RequestMappingLabelProvider(Stylers stylers, LiveExpression bde, RequestMappingsColumn column) { + this.bde = bde; + this.column = column; + this.stylers = stylers; + } + + @Override + public void update(ViewerCell cell) { + Object o = cell.getElement(); + if (o instanceof String) { + if (column==RequestMappingsColumn.SRC) { + cell.setText((String) o); + cell.setStyleRanges(null); + } else { + cell.setText(""); + } + } else if (o instanceof RequestMapping) { + StyledString styledText = getStyledText((RequestMapping)o); + if (styledText!=null) { + cell.setText(styledText.getString()); + cell.setStyleRanges(styledText.getStyleRanges()); + } else { + cell.setText(""+cell.getElement()); + cell.setStyleRanges(null); + } + } else { + cell.setText(""); + cell.setStyleRanges(null); + } + } + + protected StyledString getStyledText(RequestMapping rm) { + Styler deemphasize = Stylers.NULL; + if (!rm.isUserDefined()) { + deemphasize = stylers.grey(); + } + switch (column) { + case PATH: + String path = rm.getPath(); + String defaultPath = getDefaultPath(bde.getValue()); + if (defaultPath.equals(path)) { + return new StyledString(path, stylers.bold()); + } else { + return new StyledString(path, deemphasize); + } + case SRC: + String m = rm.getMethodString(); + if (m!=null) { + return new StyledString(m, deemphasize); + } + default: + break; + } + return new StyledString("???", deemphasize); + } + + private String getDefaultPath(BootDashElement value) { + if (value!=null) { + String path = value.getDefaultRequestMappingPath(); + if (path!=null) { + return path; + } + } + return ""; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RequestMappingsColumn.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RequestMappingsColumn.java new file mode 100644 index 000000000..48ff07626 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RequestMappingsColumn.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views; + +import org.eclipse.swt.SWT; + +public enum RequestMappingsColumn { + + PATH("Path", 80), + SRC("Source", 100); + + private int defaultWidth; + private String label; + + private RequestMappingsColumn(String label, int defaultWidth) { + this.defaultWidth = defaultWidth; + this.label= label; + } + + public String getLabel() { + return label; + } + + public int getAlignment() { + return SWT.LEFT; + } + + public int getDefaultWidth() { + return defaultWidth; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RestartDevtoolsClientAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RestartDevtoolsClientAction.java new file mode 100644 index 000000000..f766f27ce --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RestartDevtoolsClientAction.java @@ -0,0 +1,128 @@ +package org.springframework.ide.eclipse.boot.dash.views; + +import java.net.URL; +import java.util.concurrent.TimeoutException; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.PlatformUI; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springframework.ide.eclipse.boot.core.ISpringBootProject; +import org.springframework.ide.eclipse.boot.core.SpringBootCore; +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.DevtoolsConnectable; +import org.springframework.ide.eclipse.boot.dash.api.TemporalBoolean; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElement; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.common.base.Objects; + +public class RestartDevtoolsClientAction extends AbstractBootDashElementsAction { + + private ElementStateListener stateListener; + + public RestartDevtoolsClientAction(Params params) { + super(params); + this.setText("Restart Remote DevTools Client"); + this.setToolTipText("Start local devtools 'client' process for remote application."); + URL url = FileLocator.find(Platform.getBundle("org.springframework.ide.eclipse.boot"), new Path("resources/icons/boot-devtools-icon.png"), null); + if (url != null) { + this.setImageDescriptor(ImageDescriptor.createFromURL(url)); + } + if (model != null) { + model.addElementStateListener(stateListener = new ElementStateListener() { + public void stateChanged(BootDashElement e) { + if (getSelectedElements().contains(e) && !PlatformUI.getWorkbench().isClosing()) { + PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { + public void run() { + updateEnablement(); + } + }); + } + } + }); + } + } + + @Override + public void updateVisibility() { + boolean visible = false; + if (!getSelectedElements().isEmpty()) { + visible = true; + for (BootDashElement e : getSelectedElements()) { + if (!visibleForElement(e)) { + visible = false; + break; + } + } + } + setVisible(visible); + } + + private boolean visibleForElement(BootDashElement e) { + if (e instanceof GenericRemoteAppElement) { + App data = ((GenericRemoteAppElement) e).getAppData(); + return ((DevtoolsConnectable)data).isDevtoolsConnectable()!=TemporalBoolean.NEVER; + } + return false; + } + + @Override + public void updateEnablement() { + boolean enable = false; + if (!getSelectedElements().isEmpty()) { + enable = true; + for (BootDashElement e : getSelectedElements()) { + if (!enableForElement(e)) { + enable = false; + break; + } + } + } + this.setEnabled(enable); + } + + private boolean enableForElement(BootDashElement bde) { + try { + IProject project = bde.getProject(); + if (bde instanceof GenericRemoteAppElement && project!=null && bde.getRunState().isActive()) { + if (BootPropertyTester.fastHasDevTools(project)) { + App data = ((GenericRemoteAppElement)bde).getAppData(); + if (data instanceof DevtoolsConnectable) { + return ((DevtoolsConnectable)data).isDevtoolsConnectable().isTrue(); + } + } + } + } catch (TimeoutException e) { + //expected from fastHasDevTools + } catch (Exception e) { + Log.log(e); + } + return false; + } + + @Override + public void run() { + for (BootDashElement _e : getSelectedElements()) { + if (_e instanceof GenericRemoteAppElement && _e.getProject() != null) { + GenericRemoteAppElement e = (GenericRemoteAppElement) _e; + e.restartRemoteDevtoolsClient(); + } + } + } + + @Override + public void dispose() { + if (model != null && stateListener != null) { + model.removeElementStateListener(stateListener); + stateListener = null; + } + super.dispose(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RunStateAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RunStateAction.java new file mode 100644 index 000000000..e874ab804 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/RunStateAction.java @@ -0,0 +1,262 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.core.runtime.jobs.MultiRule; +import org.eclipse.swt.widgets.Display; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springsource.ide.eclipse.commons.frameworks.core.util.JobUtil; + +import com.google.common.base.Objects; + +/** + * An action who's intended effect is to transition a BootDashElement to a + * given goal state. + * + * @author Kris De Volder + */ +public abstract class RunStateAction extends AbstractBootDashElementsAction { + + private static final boolean DEBUG = false; //(""+Platform.getLocation()).contains("kdvolder"); + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + public boolean showInToolbar() { + return true; + } + + protected static class BdeSchedulingRule implements ISchedulingRule { + + private BootDashElement element; + + public BdeSchedulingRule(BootDashElement element) { + this.element = element; + } + + @Override + public boolean contains(ISchedulingRule rule) { + if (rule instanceof BdeSchedulingRule) { + BootDashElement other = ((BdeSchedulingRule) rule).element; + return Objects.equal(element, other); + } + return false; + } + + @Override + public boolean isConflicting(ISchedulingRule rule) { + if (rule instanceof BdeSchedulingRule) { + BootDashElement other = ((BdeSchedulingRule) rule).element; + if (element != null && other != null) { + return isAncestor(other, element) || isAncestor(other, element); + } + } + return false; + } + + private boolean isAncestor(BootDashElement ancestor, BootDashElement e) { + while (e != null) { + if (e.equals(ancestor)) { + return true; + } else { + Object parent = e.getParent(); + e = parent instanceof BootDashElement ? (BootDashElement) parent : null; + } + } + return false; + } + + } + + private static final ISchedulingRule SCEDULING_RULE = JobUtil.lightRule("RunStateAction.RULE"); + final RunState goalState; + private ElementStateListener stateListener = null; + + protected void configureJob(Job job) { + ISchedulingRule rule = getSelectedElements().isEmpty() ? SCEDULING_RULE + : MultiRule.combine( + getSelectedElements().stream().map(BdeSchedulingRule::new).toArray(ISchedulingRule[]::new)); + job.setRule(rule); + } + + public RunStateAction(Params params, RunState goalState) { + super(params); + debug("Create RunStateAction "+goalState); + this.goalState = goalState; + model.addElementStateListener(stateListener = new ElementStateListener() { + public void stateChanged(BootDashElement e) { + Display.getDefault().asyncExec(new Runnable() { + public void run() { + updateEnablement(); + } + }); + } + }); + } + @Override + public void updateEnablement() { + Collection selecteds = getSelectedElements(); + setEnabled(appliesTo(selecteds)); + } + + @Override + public void updateVisibility() { + /** + * TODO: Evaluate possibility of adding new API on BootDashModel and/or + * BootDashElement to check whether element supports run states. + * Currently run state == null means element doesn't support run states + */ + boolean visible = !getSelectedElements().isEmpty(); + for (BootDashElement e : getSelectedElements()) { + boolean vis = isVisibleForElement(e); + if (!vis) { + visible = false; + break; + } + } + setVisible(visible); + } + + protected boolean isVisibleForElement(BootDashElement e) { + return e.getRunState() != null && e.supportedGoalStates().contains(goalState); + } + + private boolean appliesTo(Collection selection) { + for (BootDashElement e : selection) { + if (!appliesTo(e)) { + return false; + } + } + return !selection.isEmpty(); + } + + private boolean appliesTo(BootDashElement e) { + return goalStateAppliesTo(e) && currentStateAcceptable(e.getRunState()) && appliesToElement(e); + } + + /** + * Subclass can override when action should only apply to + * certain boot dash elements + */ + protected boolean appliesToElement(BootDashElement e) { + return true; + } + + /** + * Subclass can override when action should only apply to + * processes in a specific runState. + */ + protected boolean currentStateAcceptable(RunState runState) { + return true; + } + + protected boolean goalStateAppliesTo(BootDashElement e) { + return e.supportedGoalStates().contains(goalState); + } + + + @Override + public String toString() { + return "RunStateAction("+goalState+")"; + } + + /** + * Subclass can override to define custom 'work' this action does when it is triggered. + * Default implementation just calls 'setGoalState', on all selected / applicable elements.` + */ + protected Job createJob() { + final Collection selecteds = getSelectedElements(); + if (!selecteds.isEmpty()) { + return new Job("Suspending " + selecteds.size() + " Boot Dash Elements") { + protected IStatus run(IProgressMonitor monitor) { + monitor.beginTask("Stopping " + selecteds.size() + " Elements", selecteds.size()); + try { + + List> futures = new ArrayList<>(selecteds.size()); + for (BootDashElement el : selecteds) { + if (appliesTo(el)) { + futures.add(CompletableFuture.runAsync(() -> { + try { + el.setGoalState(goalState); + monitor.worked(1); + } catch (Exception e) { + monitor.worked(1); + throw new CompletionException(e); + } + })); + } + } + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])).get(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + BootActivator.createErrorStatus(e); + } catch (ExecutionException e) { + BootActivator.createErrorStatus(e); + } catch (TimeoutException e) { + BootActivator.createErrorStatus(e); + } + + + return Status.OK_STATUS; + } finally { + monitor.done(); + } + } + }; + } + return null; + } + + public final void run() { + Job job = createJob(); + if (job!=null) { + configureJob(job); + job.schedule(); + } + } + + @Override + public void dispose() { + debug("DISPOSE RunStateAction "+getGoalState()); + super.dispose(); + if (stateListener!=null) { + //Avoid leaking model listeners + model.removeElementStateListener(stateListener); + stateListener = null; + } + } + + public RunState getGoalState() { + return goalState; + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ShowBootDashboardHandler.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ShowBootDashboardHandler.java new file mode 100644 index 000000000..6b8149969 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ShowBootDashboardHandler.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +/** + * Handler for showing Boot Dashboard view. + * + * @author Alex Boyko + * + */ +public class ShowBootDashboardHandler extends AbstractHandler { + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + new ShowViewAction(BootDashTreeView.ID).run(); + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ShowViewAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ShowViewAction.java new file mode 100644 index 000000000..e01190882 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ShowViewAction.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.views; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.views.IViewDescriptor; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.util.Log; + +/** + * Action for showing/opening a specific Eclipse view + * + * @author Alex Boyko + * + */ +public class ShowViewAction extends Action { + + private String viewId; + + public ShowViewAction(String viewId) { + this(viewId, null, null); + } + + public ShowViewAction(String viewId, String label, ImageDescriptor iconDescriptor) { + super(); + this.viewId = viewId; + IViewDescriptor viewDescriptor = PlatformUI.getWorkbench().getViewRegistry().find(viewId); + if (label == null && viewDescriptor != null) { + setText("Show " + viewDescriptor.getLabel()); + } else { + setText(label); + } + if (iconDescriptor == null && viewDescriptor != null) { + setImageDescriptor(viewDescriptor.getImageDescriptor()); + } else { + setImageDescriptor(iconDescriptor); + } + } + + @Override + public void run() { + if (viewId == null) { + BootDashActivator.getDefault().getLog().log(new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, "View ID is missing")); + } else { + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window != null && window.getActivePage() != null) { + try { + window.getActivePage().showView(viewId, null, IWorkbenchPage.VIEW_ACTIVATE); + } catch (PartInitException e) { + Log.log(e); + } + } else { + BootDashActivator.getDefault().getLog().log(new Status(IStatus.ERROR, BootDashActivator.PLUGIN_ID, "Cannot find workbench window with active page")); + } + } + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ToggleBootDashModelConnection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ToggleBootDashModelConnection.java new file mode 100644 index 000000000..5677ef40e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ToggleBootDashModelConnection.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.views; + +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.cloudfoundry.RemoteBootDashModel; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RemoteRunTarget.ConnectMode; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * Action for toggling Boot Dash model connection + * + * @author Alex Boyko + */ +public class ToggleBootDashModelConnection extends AbstractBootDashModelAction { + + protected ToggleBootDashModelConnection(LiveExpression section, SimpleDIContext context) { + super(section, context); + } + + @Override + public void update() { + super.update(); + BootDashModel model = sectionSelection.getValue(); + if (model instanceof RemoteBootDashModel) { + RemoteBootDashModel connectable = (RemoteBootDashModel) model; + if (connectable.getRunTarget().isConnected()) { + setText("Disconnect"); + setDescription("Disconnect Run Target"); + setImageDescriptor(BootDashActivator.getImageDescriptor("icons/cloud-inactive.png")); + } else { + setText("Connect"); + setDescription("Connect Run Target"); + setImageDescriptor(BootDashActivator.getImageDescriptor("icons/cloud-ready.png")); + } + } + } + + @Override + public void updateEnablement() { + setEnabled(isVisible()); + } + + @Override + public void updateVisibility() { + setVisible(sectionSelection.getValue() instanceof RemoteBootDashModel); + } + + @Override + public void run() { + BootDashModel model = sectionSelection.getValue(); + if (model instanceof RemoteBootDashModel) { + RemoteBootDashModel connectable = (RemoteBootDashModel) model; + if (connectable.getRunTarget().isConnected()) { + connectable.disconnect(); + } else { + try { + connectable.connect(ConnectMode.INTERACTIVE); + } catch (Exception e) { + //TODO: show error on the view somehow? E.g. error marker on the target? + ui().errorPopup("Failed to connect to remote target", ExceptionUtil.getMessage(e)); + Log.warn(e); + } + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ToggleFilterAction.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ToggleFilterAction.java new file mode 100644 index 000000000..ccdd1c869 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/ToggleFilterAction.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2016 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.views; + +import org.eclipse.jface.action.IAction; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel; +import org.springframework.ide.eclipse.boot.dash.model.ToggleFiltersModel.FilterChoice; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +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.ValueListener; + +import com.google.common.collect.ImmutableSet; + +/** + * A checkbox-style menu action that enables / disables + * a particular 'toggle filter'. + * + * @author Kris De Volder + */ +public class ToggleFilterAction extends AbstractBootDashAction { + + private ToggleFiltersModel toggleFilters; + private ValueListener> selectedFilterListener; + final private FilterChoice filter; + + public ToggleFilterAction(BootDashViewModel model, FilterChoice filter, SimpleDIContext context) { + super(context, IAction.AS_CHECK_BOX); + this.filter = filter; + this.toggleFilters = model.getToggleFilters(); + this.setText(filter.getLabel()); + model.getToggleFilters().getSelectedFilters().addListener(selectedFilterListener=new ValueListener>() { + public void gotValue(LiveExpression> exp, ImmutableSet value) { + updateCheckedState(); + } + }); + } + + protected void updateCheckedState() { + setChecked(getModelCheckedState()); + } + + protected boolean getModelCheckedState() { + return toggleFilters.getSelectedFilters().getValues().contains(filter); + } + + protected void setModelCheckedState(boolean checked) { + LiveSetVariable activeFilters = toggleFilters.getSelectedFilters(); + if (checked) { + activeFilters.add(filter); + } else { + activeFilters.remove(filter); + } + } + + @Override + public void run() { + setModelCheckedState(!getModelCheckedState()); + } + + @Override + public void dispose() { + if (selectedFilterListener!=null) { + toggleFilters.getSelectedFilters().removeListener(selectedFilterListener); + } + super.dispose(); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AbstractBdeGeneralPropertiesSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AbstractBdeGeneralPropertiesSection.java new file mode 100644 index 000000000..2f8dc2846 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AbstractBdeGeneralPropertiesSection.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.properties; + +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.views.properties.tabbed.ITabbedPropertyConstants; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; + +/** + * Abstract common functionality for general properties view section for the + * {@link BootDashElement} + * + * @author Alex Boyko + * + */ +public abstract class AbstractBdeGeneralPropertiesSection extends AbstractBdePropertiesSection { + + private BootDashElementPropertyControl[] propertyControls = new BootDashElementPropertyControl[0]; + + abstract protected BootDashElementPropertyControl[] createPropertyControls(); + + @Override + public void createControls(Composite parent, TabbedPropertySheetPage aTabbedPropertySheetPage) { + super.createControls(parent, aTabbedPropertySheetPage); + + BootDashElementPropertyControl[] createdControls = createPropertyControls(); + propertyControls = createdControls == null ? new BootDashElementPropertyControl[0] : createdControls; + + final Composite composite = getWidgetFactory().createFlatFormComposite(parent); + composite.setLayout(GridLayoutFactory.fillDefaults().numColumns(2).margins(ITabbedPropertyConstants.HSPACE, ITabbedPropertyConstants.VSPACE).create()); + for (BootDashElementPropertyControl control : propertyControls) { + control.createControl(composite, aTabbedPropertySheetPage); + } + } + + @Override + public void setInput(IWorkbenchPart part, ISelection selection) { + super.setInput(part, selection); + BootDashElement bootDashElement = getBootDashElement(); + for (BootDashElementPropertyControl control : propertyControls) { + control.setInput(bootDashElement); + } + } + + @Override + public void dispose() { + for (BootDashElementPropertyControl control : propertyControls) { + control.dispose(); + } + super.dispose(); + } + + @Override + public void refresh() { + super.refresh(); + for (BootDashElementPropertyControl control : propertyControls) { + control.refreshControl(); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AbstractBdePropertiesSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AbstractBdePropertiesSection.java new file mode 100644 index 000000000..51fe87b1f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AbstractBdePropertiesSection.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.views.properties; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.views.properties.tabbed.AbstractPropertySection; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; + +/** + * Abstract implementation of common functionality for {@link BootDashElement} + * properties view section + * + * @author Alex Boyko + * + */ +public abstract class AbstractBdePropertiesSection extends AbstractPropertySection implements ElementStateListener { + + private LiveVariable bde = new LiveVariable<>(); + + @Override + public void createControls(Composite parent, TabbedPropertySheetPage aTabbedPropertySheetPage) { + BootDashActivator.getDefault().getModel().addElementStateListener(this); + super.createControls(parent, aTabbedPropertySheetPage); + } + + @Override + public void setInput(IWorkbenchPart part, ISelection selection) { + super.setInput(part, selection); + + Assert.isTrue(selection instanceof IStructuredSelection); + IStructuredSelection structuredSelection = (IStructuredSelection) selection; + if (structuredSelection.size() > 1) { + bde.setValue(null); + } else { + Object inputObj = structuredSelection.getFirstElement(); + Assert.isTrue(inputObj instanceof BootDashElement); + bde.setValue((BootDashElement) inputObj); + } + } + + @Override + public void dispose() { + BootDashActivator.getDefault().getModel().removeElementStateListener(this); + super.dispose(); + } + + @Override + public void stateChanged(BootDashElement e) { + Display display = getPart() == null ? null : getPart().getSite().getShell().getDisplay(); + if (display == null) { + display = Display.getDefault(); + } + if (display != null) { + display.asyncExec(new Runnable() { + @Override + public void run() { + refresh(); + } + }); + } else { + refresh(); + } + } + + final protected BootDashElement getBootDashElement() { + return bde.getValue(); + } + + final protected LiveExpression getBootDashElementLiveExpression() { + return bde; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AbstractBdePropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AbstractBdePropertyControl.java new file mode 100644 index 000000000..a6c751779 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AbstractBdePropertyControl.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.properties; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +/** + * Abstract common implementation for the Boot Dash Element feature control for + * the properties view section + * + * @author Alex Boyko + * @author Kris De Volder + */ +public abstract class AbstractBdePropertyControl implements BootDashElementPropertyControl { + + private BootDashElement bde = null; + private Stylers stylers; + private BootDashLabels bdeLabels; + + public void setInput(BootDashElement bde) { + if (bde != this.bde) { + this.bde = bde; + } + } + + final protected BootDashElement getBootDashElement() { + return bde; + } + + final protected BootDashLabels getLabels() { + return bdeLabels; + } + + @Override + public void dispose() { + bdeLabels.dispose(); + stylers.dispose(); + } + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + stylers = new Stylers(composite.getFont()); + bdeLabels = new BootDashLabels(BootDashActivator.getDefault().getInjections(), stylers); + bdeLabels.setDeclutter(false); + } + + protected Stylers getStylers() { + return stylers; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AppPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AppPropertyControl.java new file mode 100644 index 000000000..6a4510b74 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/AppPropertyControl.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.views.properties; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; + +/** + * App control for properties view section + * + * @author Alex Boyko + * + */ +public class AppPropertyControl extends AbstractBdePropertyControl { + + private Label app; + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + page.getWidgetFactory().createLabel(composite, "Name:").setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + app = page.getWidgetFactory().createLabel(composite, ""); //$NON-NLS-1$ + app.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + } + + @Override + public void refreshControl() { + if (app != null && !app.isDisposed()) { + BootDashElement element = getBootDashElement(); + app.setText(getLabels().getStyledText(element, BootDashColumn.NAME).getString()); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/BdeReadOnlyTextPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/BdeReadOnlyTextPropertyControl.java new file mode 100644 index 000000000..15d919f4e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/BdeReadOnlyTextPropertyControl.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2016 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.views.properties; + +import java.util.function.Function; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; + +/** + * Property page control that displays the value of a read-only textual property of a {@link BootDashElement} + * + * @author Alex Boyko + * @author Kris De Volder + */ +public class BdeReadOnlyTextPropertyControl extends AbstractBdePropertyControl { + + final private String label; + final private Function getter; + final private Class type; + + public BdeReadOnlyTextPropertyControl(Class type, String label, Function getter) { + this.label = label; + this.type = type; + this.getter = getter; + } + + private Label textWidget; + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + + page.getWidgetFactory().createLabel(composite, label).setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + textWidget = page.getWidgetFactory().createLabel(composite, ""/*, SWT.BORDER*/); //$NON-NLS-1$ + textWidget.setLayoutData(GridDataFactory.fillDefaults().align(SWT.BEGINNING, SWT.FILL).create()); + } + + @Override + public void refreshControl() { + BootDashElement element = getBootDashElement(); + if (textWidget != null && !textWidget.isDisposed()) { + textWidget.setText(getPropertyValue(element)); + } + } + + protected String getPropertyValue(BootDashElement service){ + BootDashElement element = getBootDashElement(); + if (element!=null && type.isAssignableFrom(element.getClass())) { + @SuppressWarnings("unchecked") + String val = getter.apply((T)element); + if (val!=null) { + return val; + } + } + return ""; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/BeansPropertiesSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/BeansPropertiesSection.java new file mode 100644 index 000000000..11f063d2d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/BeansPropertiesSection.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2017, 2020 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.views.properties; + +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.ui.IWorkbenchPart; +import org.springframework.ide.eclipse.beans.ui.live.model.AbstractLiveBeansModelElement; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeanType; +import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansModel; +import org.springframework.ide.eclipse.beans.ui.live.tree.ContextGroupedBeansContentProvider; +import org.springframework.ide.eclipse.beans.ui.live.tree.LiveBeansTreeLabelProvider; +import org.springframework.ide.eclipse.beans.ui.live.utils.LiveBeanUtil; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.Failable; +import org.springframework.ide.eclipse.boot.dash.model.MissingLiveInfoMessages; +import org.springsource.ide.eclipse.commons.livexp.ui.util.TreeElementWrappingContentProvider; +import org.springsource.ide.eclipse.commons.livexp.ui.util.TreeElementWrappingContentProvider.TreeNode; + +/** + * Live beans property section + * + * @author Alex Boyko + * + */ +public class BeansPropertiesSection extends LiveDataPropertiesSection { + + private SearchableTreeControl searchableTree; + + @Override + public void setInput(IWorkbenchPart part, ISelection selection) { + super.setInput(part, selection); + // BootDashElement should be input rather than the LiveBeansModel. Due to + // polling the model often to show changes in the model it's best to refresh the + // tree viewer rather then set the whole input that would remove the selection + // and collapse expanded nodes + searchableTree.getTreeViewer().setInput(getBootDashElement()); + } + + private class BeansContentProvider implements ITreeContentProvider { + + private ITreeContentProvider delegateContentProvider; + + BeansContentProvider(ITreeContentProvider delegate) { + this.delegateContentProvider = delegate; + } + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof BootDashElement) { + return delegateContentProvider.getElements(data == null ? null : data.getValue()); + } + return new Object[0]; + } + + @Override + public Object[] getChildren(Object parentElement) { + return delegateContentProvider.getChildren(parentElement); + } + + @Override + public Object getParent(Object element) { + return delegateContentProvider.getParent(element); + } + + @Override + public boolean hasChildren(Object element) { + return delegateContentProvider.hasChildren(element); + } + } + + private class DoubleClickListener implements IDoubleClickListener { + + @Override + public void doubleClick(DoubleClickEvent event) { + ISelection sel = event.getSelection(); + if (sel instanceof IStructuredSelection) { + IStructuredSelection structuredSel = (IStructuredSelection) sel; + Object firstElement = structuredSel.getFirstElement(); + if (firstElement instanceof TreeNode) { + TreeNode node = (TreeNode) firstElement; + Object wrappedValue = node.getWrappedValue(); + // NOTE: navigate to bean type and navigate to resource definition are NOT + // always the same. Be sure to use the correct one given a tree node. + // For nodes that indicate bean Type, use "to type" navigation + // For all other nodes, use "to resource definition" navigation + if (wrappedValue instanceof AbstractLiveBeansModelElement) { + LiveBeanUtil.navigateToResource((AbstractLiveBeansModelElement) wrappedValue); + } else if (wrappedValue instanceof LiveBeanType) { + LiveBeanUtil.navigateToType(((LiveBeanType) wrappedValue).getBean()); + } + } + } + } + } + + @Override + protected Control createSectionDataControls(Composite parent) { + ITreeContentProvider treeContent = new TreeElementWrappingContentProvider(new BeansContentProvider(ContextGroupedBeansContentProvider.INSTANCE)); + LabelProvider labelProvider = LiveBeansTreeLabelProvider.INSTANCE; + + searchableTree = new SearchableTreeControl(getWidgetFactory()); + + searchableTree.createControls(parent, treeContent, labelProvider); + + searchableTree.getTreeViewer().addDoubleClickListener(new DoubleClickListener()); + + + return searchableTree.getComposite(); + } + + @Override + protected void refreshDataControls() { + searchableTree.refresh(); + } + + @Override + protected Failable fetchData() { + BootDashElement bde = getBootDashElement(); + if (bde == null) { + return Failable.error(MissingLiveInfoMessages.noSelectionMessage("Beans")); + } else { + return bde.getLiveBeans(); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/BootDashElementPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/BootDashElementPropertyControl.java new file mode 100644 index 000000000..57aa9674d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/BootDashElementPropertyControl.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.properties; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; + +/** + * Control for Boot Dash Element features for properties view + * + * @author Alex Boyko + * + */ +public interface BootDashElementPropertyControl { + + void createControl(Composite composite, TabbedPropertySheetPage page); + + void refreshControl(); + + void setInput(BootDashElement bde); + + void dispose(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/DebugPortPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/DebugPortPropertyControl.java new file mode 100644 index 000000000..6f7881452 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/DebugPortPropertyControl.java @@ -0,0 +1,38 @@ +package org.springframework.ide.eclipse.boot.dash.views.properties; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElement; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; + +import com.google.common.collect.ImmutableSet; + +public class DebugPortPropertyControl extends AbstractBdePropertyControl { + + private Label ports; + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + page.getWidgetFactory().createLabel(composite, "Debug Port:").setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + ports = page.getWidgetFactory().createLabel(composite, ""); //$NON-NLS-1$ + ports.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + } + + @Override + public void refreshControl() { + if (ports != null && !ports.isDisposed()) { + BootDashElement element = getBootDashElement(); + String portStr = ""; + if (element instanceof GenericRemoteAppElement) { + GenericRemoteAppElement app = (GenericRemoteAppElement) element; + ImmutableSet ports = app.getDebugPortSummary(); + portStr = StringUtil.collectionToCommaDelimitedString(ports); + } + ports.setText(portStr); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/DefaultPathPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/DefaultPathPropertyControl.java new file mode 100644 index 000000000..7c5beda20 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/DefaultPathPropertyControl.java @@ -0,0 +1,133 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.properties; + +import org.eclipse.jface.fieldassist.ContentProposalAdapter; +import org.eclipse.jface.fieldassist.IContentProposalListener2; +import org.eclipse.jface.fieldassist.TextContentAdapter; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.events.FocusAdapter; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.views.sections.RequestMappingContentProposalProvider; +import org.springframework.ide.eclipse.boot.dash.views.sections.UIUtils; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; + +/** + * Default Path control for the properties section + * + * @author Alex Boyko + * + */ +public class DefaultPathPropertyControl extends AbstractBdePropertyControl { + + private Text defaultPath; + private LiveVariable liveVar = new LiveVariable(); + private ContentProposalAdapter ca; + + /** + * Tracks whether editor value is dirty. Necessary to avoid from accidentally + * synchronizing the editor text with the model (when there are change event), + * and then loosing text we are currently typing. + */ + private boolean isDirty = false; + + final private KeyListener KEY_LISTENER = new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.doit) { + if (e.character == '\u001b') { // Escape character + defaultPath.setText(getElementText()); + e.widget.getDisplay().getActiveShell().forceFocus(); + isDirty = false; + } else if (e.character == '\r') { // Return key + e.widget.getDisplay().getActiveShell().forceFocus(); + } else { + isDirty = true; + } + } + } + }; + + @Override + public void createControl(final Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + page.getWidgetFactory().createLabel(composite, "Path:").setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + defaultPath = page.getWidgetFactory().createText(composite, ""); //$NON-NLS-1$ + defaultPath.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + defaultPath.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + save(); + } + }); + + defaultPath.addKeyListener(KEY_LISTENER); + + ca = new ContentProposalAdapter( + defaultPath, new TextContentAdapter(), + new RequestMappingContentProposalProvider(liveVar), + UIUtils.CTRL_SPACE, UIUtils.PATH_CA_AUTO_ACTIVATION_CHARS); + + ca.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE); + + ca.addContentProposalListener(new IContentProposalListener2() { + @Override + public void proposalPopupOpened(ContentProposalAdapter adapter) { + defaultPath.removeKeyListener(KEY_LISTENER); + } + @Override + public void proposalPopupClosed(ContentProposalAdapter adapter) { + defaultPath.addKeyListener(KEY_LISTENER); + } + }); + + } + + @Override + public void refreshControl() { + if (!isDirty && defaultPath != null && !defaultPath.isDisposed()) { + defaultPath.setText(getElementText()); + } + } + + private void save() { + BootDashElement bde = getBootDashElement(); + if (bde != null && !defaultPath.getText().equals(getElementText())) { + bde.setDefaultRequestMappingPath(defaultPath.getText()); + } + isDirty = false; + } + + protected String getElementText() { + BootDashElement e = getBootDashElement(); + if (e!=null) { + String str = e.getDefaultRequestMappingPath(); + if (str!=null) { + return str; + } + } + return ""; + } + + @Override + public void setInput(BootDashElement bde) { + super.setInput(bde); + liveVar.setValue(bde); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/EnvPropertiesSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/EnvPropertiesSection.java new file mode 100644 index 000000000..039c7f01f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/EnvPropertiesSection.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * Copyright (c) 2019, 2020 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.views.properties; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.ui.IWorkbenchPart; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.Failable; +import org.springframework.ide.eclipse.boot.dash.model.MissingLiveInfoMessages; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.ActiveProfiles; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.LiveEnvModel; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.Property; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.PropertyOrigin; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.PropertySource; +import org.springframework.ide.eclipse.boot.dash.model.actuator.env.PropertySources; +import org.springsource.ide.eclipse.commons.livexp.ui.util.TreeElementWrappingContentProvider; + +/** + * Live env property section + * + * + */ +public class EnvPropertiesSection extends LiveDataPropertiesSection { + + private SearchableTreeControl searchableTree; + + @Override + public void setInput(IWorkbenchPart part, ISelection selection) { + super.setInput(part, selection); + searchableTree.getTreeViewer().setInput(getBootDashElement()); + } + + private class LiveEnvContentProvider implements ITreeContentProvider { + + LiveEnvContentProvider() { + } + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof BootDashElement) { + LiveEnvModel liveEnv = data.hasFailed() ? null : data.getValue(); + if (liveEnv != null) { + List elements = new ArrayList<>(); + + ActiveProfiles activeProfiles = liveEnv.getActiveProfiles(); + if (activeProfiles != null) { + elements.add(activeProfiles); + } + PropertySources propertySources = liveEnv.getPropertySources(); + if (propertySources != null) { + elements.add(propertySources); + } + return elements.toArray(); + } + } + return new Object[0]; + } + + @Override + public Object[] getChildren(Object parentElement) { + + if (parentElement instanceof ActiveProfiles) { + return ((ActiveProfiles) parentElement).getProfiles().toArray(); + } else if (parentElement instanceof PropertySources) { + return ((PropertySources) parentElement).getPropertySources().toArray(); + } else if (parentElement instanceof PropertySource) { + return ((PropertySource) parentElement).getProperties().toArray(); + } else if (parentElement instanceof Property) { + Property property = (Property) parentElement; + PropertyOrigin origin = property.getOrigin(); + if (origin != null) { + return new Object[] { origin }; + } + } + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + return new Object[0]; + } + + @Override + public boolean hasChildren(Object element) { + Object[] children = getChildren(element); + return children != null && children.length > 0; + } + } + + @Override + protected Control createSectionDataControls(Composite parent) { + LabelProvider labelProvider = new LiveEnvLabelProvider(); + ITreeContentProvider treeContentProvider = new TreeElementWrappingContentProvider(new LiveEnvContentProvider()); + + searchableTree = new SearchableTreeControl(getWidgetFactory()); + + searchableTree.createControls(parent, treeContentProvider, labelProvider); + + return searchableTree.getComposite(); + } + + @Override + protected void refreshDataControls() { + searchableTree.refresh(); + } + + @Override + protected Failable fetchData() { + BootDashElement bde = getBootDashElement(); + if (bde == null) { + return Failable.error(MissingLiveInfoMessages.noSelectionMessage("environment properties")); + } else { + return bde.getLiveEnv(); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ExposedPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ExposedPropertyControl.java new file mode 100644 index 000000000..15aac5385 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ExposedPropertyControl.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views.properties; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.AbstractLaunchConfigurationsDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKClient; +import org.springframework.ide.eclipse.boot.dash.ngrok.NGROKLaunchTracker; +import org.springsource.ide.eclipse.commons.ui.UiUtil; + +import com.google.common.collect.ImmutableSet; + +/** + * @author Martin Lippert + */ +public class ExposedPropertyControl extends AbstractBdePropertyControl { + + private Link exposedURL; + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + page.getWidgetFactory().createLabel(composite, "Exposed via:").setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + + exposedURL = new Link(composite, SWT.NONE); + exposedURL.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + exposedURL.setBackground(composite.getBackground()); + + exposedURL.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event event) { + AbstractLaunchConfigurationsDashElement bde = getLocalBootDashElement(); + if (bde != null) { + ImmutableSet launchConfigs = bde.getLaunchConfigs(); + if (launchConfigs.size() == 1) { + String tunnelName = launchConfigs.iterator().next().getName(); + NGROKClient ngrokClient = NGROKLaunchTracker.get(tunnelName); + if (ngrokClient != null) { + String addr = ngrokClient.getURL(); + UiUtil.openUrl(addr); + } + } + } + } + }); + } + + @Override + public void refreshControl() { + AbstractLaunchConfigurationsDashElement bde = getLocalBootDashElement(); + + if (bde != null) { + StringBuilder labelText = new StringBuilder(); + + ImmutableSet configs = bde.getLaunchConfigs(); + if (configs.size() == 1) { + String tunnelName = configs.iterator().next().getName(); + NGROKClient ngrokClient = NGROKLaunchTracker.get(tunnelName); + if (ngrokClient != null) { + labelText.append(ngrokClient.getTunnel().getPublic_url() + " --- (local ngrok instance at: " + ngrokClient.getURL() + ")"); + } + } + + exposedURL.setText(labelText.toString()); + } + } + + private AbstractLaunchConfigurationsDashElement getLocalBootDashElement() { + if (exposedURL != null && !exposedURL.isDisposed()) { + BootDashElement bde = getBootDashElement(); + if (bde instanceof AbstractLaunchConfigurationsDashElement) { + return (AbstractLaunchConfigurationsDashElement) bde; + } + } + + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/GenericRemoteAppGeneralPropertiesSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/GenericRemoteAppGeneralPropertiesSection.java new file mode 100644 index 000000000..110ecc35d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/GenericRemoteAppGeneralPropertiesSection.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views.properties; + +import org.springframework.ide.eclipse.boot.dash.api.App; +import org.springframework.ide.eclipse.boot.dash.api.JmxConnectable; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.remote.GenericRemoteAppElement; + +public class GenericRemoteAppGeneralPropertiesSection extends AbstractBdeGeneralPropertiesSection { + + @Override + protected BootDashElementPropertyControl[] createPropertyControls() { + return new BootDashElementPropertyControl[] { + new RunStatePropertyControl(), + new InstancesPropertyControl(), + new ProjectPropertyControl(), + new AppPropertyControl(), + new UrlPropertyControl<>(BootDashElement.class, "URL:", (e) -> e.getUrl()), + new DefaultPathPropertyControl(), + new TagsPropertyControl(), + new DebugPortPropertyControl(), + new ReadOnlyStringPropertyControl<>(GenericRemoteAppElement.class, "Jmx URL:", (e) -> e.getJmxUrl(), false) + .visibleWhen((e) -> e!=null && e.getAppData() instanceof JmxConnectable) + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ImageAnimation.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ImageAnimation.java new file mode 100644 index 000000000..25a2cad22 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ImageAnimation.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.properties; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.progress.UIJob; + +/** + * Images animation implementation. Changes images with a given frequency on some control + * + * @author Alex Boyko + * + */ +public abstract class ImageAnimation { + + private UIJob animateJob; + private boolean running; + + public ImageAnimation(final Image[] images, final long frequency) { + this.animateJob = new UIJob("Image Animation Job") { + + private int counter = 0; + + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + if (images == null || images.length == 0) { + setFrame(null); + } else if (images.length == 1) { + setFrame(images[0]); + } else { + setFrame(images[counter % images.length]); + counter++; + schedule(frequency); + } + return Status.OK_STATUS; + } + + }; + this.animateJob.setSystem(true); + this.running = false; + } + + public synchronized void start() { + if (!running) { + animateJob.schedule(); + running = true; + } + } + + public synchronized void stop() { + if (running) { + animateJob.cancel(); + running = false; + } + } + + abstract protected void setFrame(Image image); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/InstancesPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/InstancesPropertyControl.java new file mode 100644 index 000000000..7efafad99 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/InstancesPropertyControl.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.views.properties; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; + +/** + * Control for Instances of the app for properties section + * + * @author Alex Boyko + * + */ +public class InstancesPropertyControl extends AbstractBdePropertyControl { + + private Label instances; + private Label nameLabel; + private Composite composite; + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + this.composite = composite; + nameLabel = page.getWidgetFactory().createLabel(composite, "Instances:"); + nameLabel.setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + instances = page.getWidgetFactory().createLabel(composite, ""); //$NON-NLS-1$ + instances.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + } + + @Override + public void refreshControl() { + if (instances != null && !instances.isDisposed()) { + BootDashElement element = getBootDashElement(); + instances.setText(getLabels().getStyledText(element, BootDashColumn.INSTANCES).getString()); + } + } + + private void relayout() { + Composite c = composite; + while (c != null) { + c.layout(true, true); + c = c.getParent(); + } + } + + // For future possible dynamic filtering of controls + private void hide() { + nameLabel.setLayoutData(GridDataFactory.swtDefaults().exclude(true).create()); //$NON-NLS-1$ + nameLabel.setVisible(false); + instances.setLayoutData(GridDataFactory.swtDefaults().exclude(true).create()); + instances.setVisible(false); + relayout(); + } + + private void show() { + nameLabel.setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + nameLabel.setVisible(true); + instances.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + instances.setVisible(true); + relayout(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LiveDataPropertiesSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LiveDataPropertiesSection.java new file mode 100644 index 000000000..76d2cd42b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LiveDataPropertiesSection.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * Copyright (c) 2020 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.views.properties; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.browser.Browser; +import org.eclipse.swt.browser.LocationEvent; +import org.eclipse.swt.browser.LocationListener; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.ui.views.properties.tabbed.ITabbedPropertyConstants; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.Failable; +import org.springframework.ide.eclipse.boot.dash.model.MissingLiveInfoMessages; +import org.springsource.ide.eclipse.commons.ui.UiUtil; + +public abstract class LiveDataPropertiesSection extends AbstractBdePropertiesSection { + + protected Failable data = Failable.error(MissingLiveInfoMessages.NOT_YET_COMPUTED); + + protected TabbedPropertySheetPage page; + private Composite composite; + private Composite missingInfo; + private Browser browser; + private StackLayout layout; + + private Control dataControl; + + private static final LocationListener HYPER_LINK_LISTENER = new LocationListener() { + + @Override + public void changing(LocationEvent event) { + if (!"about:blank".equals(event.location)) { //$NON-NLS-1$ + UiUtil.openUrl(event.location); + event.doit = false; + } + } + + @Override + public void changed(LocationEvent event) { + // comment requested by sonar + } + }; + + final public void createControls(Composite parent, TabbedPropertySheetPage aTabbedPropertySheetPage) { + super.createControls(parent, aTabbedPropertySheetPage); + this.page = aTabbedPropertySheetPage; + composite = getWidgetFactory().createComposite(parent, SWT.NONE); + + // Layout variant to have owner composite size to be equal the client area size of the next upper level ScrolledComposite + composite.setLayout(layout = new SectionStackLayout()); + + layout.marginWidth = ITabbedPropertyConstants.HSPACE; + layout.marginHeight = ITabbedPropertyConstants.VSPACE; + + missingInfo = getWidgetFactory().createComposite(composite, SWT.NONE); + missingInfo.setLayout(new FillLayout()); + + browser = new Browser(missingInfo, SWT.NONE); + browser.addLocationListener(HYPER_LINK_LISTENER); + + this.dataControl = createSectionDataControls(composite); + } + + @Override + public void dispose() { + try { + if (browser != null && !browser.isDisposed()) { + browser.removeLocationListener(HYPER_LINK_LISTENER); + browser = null; + } + } catch (SWTException e) { + //See: https://www.pivotaltracker.com/story/show/174560730 + //Despite the 'isDisposed' check we still sometimes get a 'widget is disposed' exception. + //Not sure why... but it should be harmless to ignore this. + } + super.dispose(); + } + + public void refresh() { + this.data = fetchData(); + + if (data.hasFailed()) { + layout.topControl = this.missingInfo; + String newText = this.data.getErrorMessage().toHtml(); + if (browser != null && !browser.isDisposed() && !newText.equals(browser.getText())) { + browser.setText(newText); + } + } else { + layout.topControl = this.dataControl; + } + + refreshDataControls(); + + SectionStackLayout.reflow(page); + } + + protected abstract Control createSectionDataControls(Composite composite); + + protected abstract void refreshDataControls(); + + protected abstract Failable fetchData(); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LiveEnvLabelProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LiveEnvLabelProvider.java new file mode 100644 index 000000000..8b6d60b61 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LiveEnvLabelProvider.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 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.views.properties; + +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.beans.ui.live.model.DisplayName; +import org.springsource.ide.eclipse.commons.livexp.ui.util.TreeElementWrappingContentProvider; + +public class LiveEnvLabelProvider extends LabelProvider { + + @Override + public Image getImage(Object element) { + return super.getImage(element); + } + + @Override + public String getText(Object element) { + if (element instanceof TreeElementWrappingContentProvider.TreeNode) { + return getText(((TreeElementWrappingContentProvider.TreeNode) element).getWrappedValue()); + } else if (element instanceof DisplayName) { + return ((DisplayName) element).getDisplayName(); + } + return super.getText(element); + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LocalCloudServiceGeneralPropertiesSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LocalCloudServiceGeneralPropertiesSection.java new file mode 100644 index 000000000..b8fc821f6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LocalCloudServiceGeneralPropertiesSection.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views.properties; + +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; + +/** + * General properties section for Spring Cloud CLI local cloud service + * + * @author Alex Boyko + * + */ +public class LocalCloudServiceGeneralPropertiesSection extends AbstractBdeGeneralPropertiesSection { + + @Override + protected BootDashElementPropertyControl[] createPropertyControls() { + return new BootDashElementPropertyControl[] { + new RunStatePropertyControl(), + new UrlPropertyControl<>(BootDashElement.class, "URL:", (e) -> e.getUrl()), + new TagsPropertyControl(), + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LocalGeneralPropertiesSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LocalGeneralPropertiesSection.java new file mode 100644 index 000000000..e2962c8b8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/LocalGeneralPropertiesSection.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.views.properties; + +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; + +/** + * General properties section for Local apps + * + * @author Alex Boyko + * + */ +public class LocalGeneralPropertiesSection extends AbstractBdeGeneralPropertiesSection { + + @Override + protected BootDashElementPropertyControl[] createPropertyControls() { + return new BootDashElementPropertyControl[] { + new RunStatePropertyControl(), + new InstancesPropertyControl(), + new ProjectPropertyControl(), + new AppPropertyControl(), + new UrlPropertyControl<>(BootDashElement.class, "URL:", (e) -> e.getUrl()), + new DefaultPathPropertyControl(), + new TagsPropertyControl(), + new ExposedPropertyControl() + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ProjectPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ProjectPropertyControl.java new file mode 100644 index 000000000..fd66e100f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ProjectPropertyControl.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.properties; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.custom.CLabel; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; + +/** + * Controls for Project for the properties section + * + * @author Alex Boyko + * + */ +public class ProjectPropertyControl extends AbstractBdePropertyControl { + + private CLabel project; + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + page.getWidgetFactory().createLabel(composite, "Project:").setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + project = page.getWidgetFactory().createCLabel(composite, ""); //$NON-NLS-1$ + project.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + project.setBottomMargin(0); + project.setLeftMargin(0); + project.setRightMargin(0); + project.setTopMargin(0); + } + + @Override + public void refreshControl() { + if (project != null && !project.isDisposed()) { + BootDashElement bde = getBootDashElement(); + project.setText(getLabels().getStyledText(bde, BootDashColumn.PROJECT).getString()); + project.setImage(getLabels().getImage(bde, BootDashColumn.PROJECT)); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/PropertiesTitleLabelProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/PropertiesTitleLabelProvider.java new file mode 100644 index 000000000..efeda416f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/PropertiesTitleLabelProvider.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.views.properties; + +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +/** + * Label provider for Boot Dash elements for the tabbed properties view + * + * @author Alex Boyko + * + */ +public class PropertiesTitleLabelProvider implements ILabelProvider { + + BootDashLabels labels = new BootDashLabels(BootDashActivator.getDefault().getInjections(), new Stylers(null)); + + @Override + public void addListener(ILabelProviderListener listener) { + + } + + @Override + public void dispose() { + + } + + @Override + public boolean isLabelProperty(Object element, String property) { + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + + } + + @Override + public Image getImage(Object element) { + if (element instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) element; + if (selection.size() == 1) { + Object o = selection.getFirstElement(); + if (o instanceof BootDashElement) { + return ((BootDashElement) o).getPropertiesTitleIconImage(); + } + } + } + return null; + } + + @Override + public String getText(Object element) { + if (element instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) element; + if (selection.size() > 1) { + return selection.size() + " of elements are selected"; + } else { + Object o = ((IStructuredSelection)element).getFirstElement(); + if (o instanceof BootDashElement) { + StyledString l = labels.getStyledText((BootDashElement)o, BootDashColumn.NAME); + if (l!=null) { + return l.getString(); + } else { + return ((BootDashElement) o).getName(); + } + } + } + } + return null; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ReadOnlyStringPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ReadOnlyStringPropertyControl.java new file mode 100644 index 000000000..dab6cb3cf --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/ReadOnlyStringPropertyControl.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * Copyright (c) 2016, 2020 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.views.properties; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; + +import com.google.common.base.Predicate; + +/** + * URL control for property section composite + * + * @author Kris De Volder + */ +public class ReadOnlyStringPropertyControl extends AbstractBdePropertyControl { + + private static final String PLACEHOLDER_TEXT = "Getting value..."; + private final String labelString; + private StyledText value; + + private final Class type; + private final Function getter; + private final boolean asyncRefresh; + private Predicate visibleWhen = x -> true; + private Label label; + + public ReadOnlyStringPropertyControl(Class type, String label, Function getter, boolean asyncRefresh) { + this.type = type; + this.labelString = label; + this.getter = getter; + this.asyncRefresh = asyncRefresh; + } + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + + label = page.getWidgetFactory().createLabel(composite, labelString); + label.setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + value = new StyledText(composite, SWT.READ_ONLY); + value.setCaret(null); + value.setText(PLACEHOLDER_TEXT); + value.setStyleRange(new StyleRange(0, PLACEHOLDER_TEXT.length(), value.getDisplay().getSystemColor(SWT.COLOR_GRAY), null)); + value.setLayoutData(GridDataFactory.fillDefaults().align(SWT.BEGINNING, SWT.FILL).create()); + } + + @Override + public void refreshControl() { + BootDashElement element = getBootDashElement(); + // This must on the UI thread + final Display display = Display.getCurrent(); + if (asyncRefresh) { + CompletableFuture.supplyAsync(() -> fetchValue(element)).thenAccept(text -> { + if (!display.isDisposed()) { + display.asyncExec(() -> updateUI(text)); + } + }); + } else { + updateUI(fetchValue(element)); + } + } + + private void updateUI(String text) { + boolean show = shouldShow(); + if (show) { + if (value != null && !value.isDisposed()) { + value.setText(text); + int width = value.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; + GridData data = GridDataFactory.copyData(((GridData) value.getLayoutData())); + data.widthHint = width; + value.setLayoutData(data); + value.getParent().layout(); + } + show(); + } else { + hide(); + } + } + + private boolean shouldShow() { + BootDashElement element = getBootDashElement(); + if (element!=null && type.isAssignableFrom(element.getClass())) { + @SuppressWarnings("unchecked") + Boolean val = visibleWhen.apply((T)element); + if (val!=null) { + return val; + } + } + return false; + } + + protected String fetchValue(BootDashElement element) { + if (element!=null && type.isAssignableFrom(element.getClass())) { + @SuppressWarnings("unchecked") + String val = getter.apply((T)element); + if (val!=null) { + return val; + } + } + return ""; + } + + public ReadOnlyStringPropertyControl visibleWhen(Predicate visibleWhen) { + this.visibleWhen = visibleWhen; //TODO: support this + return this; + } + + // For future possible dynamic filtering of controls + private void hide() { + label.setLayoutData(GridDataFactory.swtDefaults().exclude(true).create()); //$NON-NLS-1$ + label.setVisible(false); + value.setLayoutData(GridDataFactory.swtDefaults().exclude(true).create()); + value.setVisible(false); + relayout(); + } + + private void relayout() { + Composite c = label.getParent(); + while (c != null) { + c.layout(true, true); + c = c.getParent(); + } + } + + private void show() { + label.setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + label.setVisible(true); + value.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + value.setVisible(true); + relayout(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/RequestMappingPropertiesSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/RequestMappingPropertiesSection.java new file mode 100644 index 000000000..2ee086218 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/RequestMappingPropertiesSection.java @@ -0,0 +1,250 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.views.properties; + +import static org.springsource.ide.eclipse.commons.ui.UiUtil.openUrl; + +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IMenuListener; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.jface.viewers.ViewerComparator; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.ui.IWorkbenchPart; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.Failable; +import org.springframework.ide.eclipse.boot.dash.model.MissingLiveInfoMessages; +import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping; +import org.springframework.ide.eclipse.boot.dash.util.Utils; +import org.springframework.ide.eclipse.boot.dash.views.RequestMappingLabelProvider; +import org.springframework.ide.eclipse.boot.dash.views.RequestMappingsColumn; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; +import org.springsource.ide.eclipse.commons.ui.SpringUIUtils; + +import com.google.common.collect.ImmutableList; + +/** + * Tabbed properties view section for live request mappings + * + * @author Alex Boyko + * @author Kris De Volder + */ +public class RequestMappingPropertiesSection extends LiveDataPropertiesSection> { + + private class DoubleClickListener extends MouseAdapter { + DoubleClickListener(TableViewer tv) { + tv.getTable().addMouseListener(this); + } + + @Override + public void mouseDoubleClick(MouseEvent e) { + ViewerCell cell = tv.getCell(new Point(e.x, e.y)); + if (cell!=null) { + Object clicked = cell.getElement(); + if (clicked instanceof RequestMapping){ + RequestMapping rm = (RequestMapping) clicked; + int colIdx = cell.getColumnIndex(); + RequestMappingsColumn col = RequestMappingsColumn.values()[colIdx]; + switch (col) { + case PATH: + BootDashElement bde = getBootDashElement(); + String url = Utils.createUrl(bde.getLiveHost(), bde.getLivePort(), rm.getPath()); + if (url!=null) { + openUrl(url); + } + break; + case SRC: + IJavaElement javaElement = rm.getMethod(); + if (javaElement == null) { + javaElement = rm.getType(); + } + + if (javaElement != null) { + SpringUIUtils.openInEditor(javaElement); + } + default: + break; + } + // MessageDialog.openInformation(page.getShell(), "clickety click!", + // "Double-click on : "+ clicked); + } + } + } + + } + + private TableViewer tv; + private RequestMappingLabelProvider labelProvider; + private Stylers stylers; + private ViewerComparator sorter = new ViewerComparator() { + + @Override + public int compare(Viewer viewer, Object e1, Object e2) { + if (e1 instanceof RequestMapping && e2 instanceof RequestMapping) { + RequestMapping rm1 = (RequestMapping) e1; + RequestMapping rm2 = (RequestMapping) e2; + int cat1 = getCategory(rm1); + int cat2 = getCategory(rm2); + if (cat1!=cat2) { + return cat1-cat2; + } else { + return rm1.getPath().compareTo(rm2.getPath()); + } + } + return 0; + } + + private int getCategory(RequestMapping rm) { + if (rm.isUserDefined()) { + return 0; + } else { + return 1; + } + } + }; + + public void setInput(IWorkbenchPart part, ISelection selection) { + super.setInput(part, selection); + tv.setInput(getBootDashElement()); + } + + @Override + public void dispose() { + if (labelProvider!=null) { + labelProvider.dispose(); + labelProvider = null; + } + if (stylers!=null) { + stylers.dispose(); + stylers = null; + } + super.dispose(); + } + + public class ContentProvider implements IStructuredContentProvider { + + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + /* + * Nothing. Rely on the section refresh mechanism that should refresh the table + */ + } + + @Override + public Object[] getElements(Object inputElement) { + return data.hasFailed() ? new Object[0] : data.getValue().toArray(); + } + } + + private void createContextMenu(Viewer viewer) { + MenuManager contextMenu = new MenuManager("#ViewerMenu"); //$NON-NLS-1$ + contextMenu.setRemoveAllWhenShown(true); + contextMenu.addMenuListener(new IMenuListener() { + @Override + public void menuAboutToShow(IMenuManager mgr) { + fillContextMenu(mgr); + } + + }); + + Menu menu = contextMenu.createContextMenu(viewer.getControl()); + viewer.getControl().setMenu(menu); + } + + private void fillContextMenu(IMenuManager contextMenu) { + if (getStructuredSelection().size() == 1) { + final RequestMapping rm = (RequestMapping) getStructuredSelection().getFirstElement(); + final BootDashElement bde = getBootDashElement(); + Action makeDefaultAction = new Action("Make Default") { + @Override + public void run() { + bde.setDefaultRequestMappingPath(rm.getPath()); + tv.refresh(); + /* + * Just refresh doesn't cause repaint for some reason + */ + tv.getTable().redraw(); + } + }; + makeDefaultAction.setEnabled(!rm.getPath().equals(bde.getDefaultRequestMappingPath())); + contextMenu.add(makeDefaultAction); + } + } + + private IStructuredSelection getStructuredSelection() { + //Watch out, this is not Eclipse 4.4 api: + //return tv.getStructuredSelection(); + //So do this instead: + return (IStructuredSelection) tv.getSelection(); + } + + @Override + protected Control createSectionDataControls(Composite composite) { + this.tv = new TableViewer(composite, SWT.BORDER|SWT.FULL_SELECTION/*|SWT.NO_SCROLL*/); + + tv.setContentProvider(new ContentProvider()); + tv.setComparator(sorter); +// tv.setLabelProvider(labelProvider = new RequestMappingLabelProvider(tv.getTable().getFont(), input)); + tv.setInput(getBootDashElement()); + tv.getTable().setHeaderVisible(true); + stylers = new Stylers(tv.getTable().getFont()); + + for (RequestMappingsColumn colType : RequestMappingsColumn.values()) { + TableViewerColumn col = new TableViewerColumn(tv, colType.getAlignment()); + col.setLabelProvider(new RequestMappingLabelProvider(stylers, getBootDashElementLiveExpression(), colType)); + TableColumn colWidget = col.getColumn(); + colWidget.setText(colType.getLabel()); + colWidget.setWidth(colType.getDefaultWidth()); + } + + createContextMenu(tv); + + new DoubleClickListener(tv); + + return tv.getControl(); + } + + @Override + protected void refreshDataControls() { + tv.refresh(); + } + + @Override + protected Failable> fetchData() { + BootDashElement bde = getBootDashElement(); + if (bde != null) { + return bde.getLiveRequestMappings(); + } else { + return Failable.error(MissingLiveInfoMessages.noSelectionMessage("Request Mapping")); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/RunStatePropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/RunStatePropertyControl.java new file mode 100644 index 000000000..ff4bf0e8c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/RunStatePropertyControl.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.properties; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.custom.CLabel; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.RunState; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; + +/** + * Controls for Run State for the properties section + * + * @author Alex Boyko + * + */ +public class RunStatePropertyControl extends AbstractBdePropertyControl { + + private static final long ANIMATION_FRAME = 100; + + private CLabel runState; + private ImageAnimation animation = null; + private RunState previousRunState = null; + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + page.getWidgetFactory().createLabel(composite, "State:").setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + runState = page.getWidgetFactory().createCLabel(composite, ""); //$NON-NLS-1$ + runState.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + runState.setBottomMargin(0); + runState.setLeftMargin(0); + runState.setRightMargin(0); + runState.setTopMargin(0); + } + + @Override + public void refreshControl() { + if (runState != null && !runState.isDisposed()) { + BootDashElement bde = getBootDashElement(); + runState.setText(getLabels().getStyledText(bde, BootDashColumn.RUN_STATE_ICN).getString()); + + if (bde == null) { + runState.setImage(null); + } else { + if (previousRunState != bde.getRunState()) { + previousRunState = bde.getRunState(); + final Image[] images = getLabels().getImageAnimation(bde, BootDashColumn.RUN_STATE_ICN); + if (animation != null) { + animation.stop(); + animation = null; + } + if (images == null || images.length == 0) { + runState.setImage(null); + } else if (images.length == 1) { + runState.setImage(images[0]); + } else { + animation = new ImageAnimation(images, ANIMATION_FRAME) { + @Override + protected void setFrame(Image image) { + runState.setImage(image); + } + }; + animation.start(); + } + } + } + } + } + + @Override + public void dispose() { + if (animation != null) { + animation.stop(); + } + super.dispose(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/SearchableTreeControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/SearchableTreeControl.java new file mode 100644 index 000000000..b429422a5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/SearchableTreeControl.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (c) 2017, 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.views.properties; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.springsource.ide.eclipse.commons.livexp.core.FilterBoxModel; +import org.springsource.ide.eclipse.commons.livexp.ui.util.SwtConnect; +import org.springsource.ide.eclipse.commons.livexp.util.Filter; +import org.springsource.ide.eclipse.commons.livexp.util.Filters; + +/** + * Tree viewer control with search box that searches for any text pattern in the + * tree. This control is specifically meant for a page container. + * + *

+ * + * Will switch between a simple message label control and the searchable tree + * control if no content is found on refresh. Therefore a no-content message + * supplier is required. + * + * @author Alex Boyko + * @author Nieraj Singh + * + */ +public class SearchableTreeControl { + + private TreeViewer treeViewer; + private Text searchBox; + private Composite treeViewerComposite; + + private final FormToolkit widgetFactory; + + /** + * + * @param widgetFactory + * @param missingContentSupplier supplies a message if no content for tree is + * available. Supplier MUST return null if content + * is available. + */ + public SearchableTreeControl(FormToolkit widgetFactory) { + this.widgetFactory = widgetFactory; + } + + public void createControls(Composite parent, ITreeContentProvider treeContentProvider, LabelProvider labelProvider) { + + treeViewerComposite = new Composite(parent, SWT.NONE); + treeViewerComposite + .setLayout(GridLayoutFactory.fillDefaults().margins(0, 0).spacing(1, 1).numColumns(1).create()); + + searchBox = widgetFactory.createText(treeViewerComposite, "", + SWT.SINGLE | SWT.BORDER | SWT.SEARCH | SWT.ICON_CANCEL); + searchBox.setMessage("Enter search string"); + GridDataFactory.fillDefaults().grab(true, false).applyTo(searchBox); + FilterBoxModel searchBoxModel = new FilterBoxModel() { + @Override + protected Filter createFilterForInput(String pattern) { + return Filters.caseInsensitiveSubstring(pattern); + } + }; + SwtConnect.connect(searchBox, searchBoxModel.getText()); + searchBox.addDisposeListener(de -> searchBoxModel.close()); + + treeViewer = new TreeViewer(treeViewerComposite /* , SWT.NO_SCROLL */); + + treeViewer.setContentProvider(treeContentProvider); + treeViewer.setLabelProvider(labelProvider); + treeViewer.setAutoExpandLevel(2); + + SwtConnect.connectTextBasedFilter(treeViewer, searchBoxModel.getFilter(), labelProvider, treeContentProvider); + GridDataFactory.fillDefaults().grab(true, true).applyTo(treeViewer.getTree()); + + } + + public Composite getComposite() { + return treeViewerComposite; + } + + public TreeViewer getTreeViewer() { + return treeViewer; + } + + public void refresh() { + // If tree is populated for the first time then auto expand to level 2 manually + // because new input is not set in this case + boolean firstTimeTreePopulated = treeViewer.getTree().getItems().length == 0; + treeViewer.refresh(); + + if (firstTimeTreePopulated) { + treeViewer.expandToLevel(2); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/SectionStackLayout.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/SectionStackLayout.java new file mode 100644 index 000000000..49d495ef4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/SectionStackLayout.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2018 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.views.properties; + +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; + +/** + * Layout variant to have owner composite size to be equal the client area size + * of the next upper level ScrolledComposite + * + * @author Alex Boyko + * + */ +class SectionStackLayout extends StackLayout { + + /** + * Resets the layout for controls inside properties page. Also recalculates scroll-bars + * @param page properties page + */ + static void reflow(TabbedPropertySheetPage page) { + final Composite target = page.getControl().getParent(); + if (target!=null) { + target.getDisplay().asyncExec(new Runnable() { + public void run() { + if (!target.isDisposed()) { + target.layout(true, true); + page.resizeScrolledComposite(); + } + } + }); + } + } + + @Override + protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) { + Point size = super.computeSize(composite, wHint, hHint, flushCache); + Composite container = getScrolledComposite(composite.getParent()); + Rectangle r = container.getClientArea(); + size = new Point(r.width, r.height); + return size; + } + + /** + * Searches for the upper level ScrolledComposite. Returns the passed composite if not found. + * @param composite + * @return + */ + private Composite getScrolledComposite(Composite composite) { + Composite c = composite; + while(c != null && !(c instanceof ScrolledComposite)) { + c = c.getParent(); + } + return c == null ? composite : c; + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/TagsPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/TagsPropertyControl.java new file mode 100644 index 000000000..749475ffa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/TagsPropertyControl.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.properties; + +import java.util.Arrays; +import java.util.LinkedHashSet; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.viewers.ICellEditorListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.TagUtils; +import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; +import org.springframework.ide.eclipse.boot.dash.views.sections.TagContentProposalProvider; +import org.springframework.ide.eclipse.boot.dash.views.sections.TagsEditor; +import org.springframework.ide.eclipse.boot.dash.views.sections.UIUtils; + +/** + * Controls for Tags for the properties section + * + * @author Alex Boyko + * + */ +public class TagsPropertyControl extends AbstractBdePropertyControl { + + private TagsEditor tags; + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + page.getWidgetFactory().createLabel(composite, "Tags:").setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + tags = new TagsEditor(composite, getStylers(), new TagContentProposalProvider(BootDashActivator.getDefault().getModel()), UIUtils.CTRL_SPACE, UIUtils.TAG_CA_AUTO_ACTIVATION_CHARS); + tags.getControl().setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + tags.addListener(new ICellEditorListener() { + @Override + public void applyEditorValue() { + String str = (String) tags.getValue(); + BootDashElement element = getBootDashElement(); + if (element != null) { + LinkedHashSet tagsSet = str.isEmpty() ? new LinkedHashSet() + : new LinkedHashSet(Arrays.asList(TagUtils.parseTags(str))); + if (!tagsSet.equals(element.getTags())) { + element.setTags(tagsSet); + } + } + } + @Override + public void cancelEditor() { + tags.setValue(getLabels().getStyledText(getBootDashElement(), BootDashColumn.TAGS).getString()); + } + @Override + public void editorValueChanged(boolean oldValidState, boolean newValidState) { + } + }); + } + + @Override + public void refreshControl() { + if (tags != null && !tags.getControl().isDisposed()) { + tags.setValue(getLabels().getStyledText(getBootDashElement(), BootDashColumn.TAGS).getString()); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/UrlPropertyControl.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/UrlPropertyControl.java new file mode 100644 index 000000000..80883481f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/properties/UrlPropertyControl.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2016 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.views.properties; + +import java.util.function.Function; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.events.HyperlinkAdapter; +import org.eclipse.ui.forms.events.HyperlinkEvent; +import org.eclipse.ui.forms.widgets.Hyperlink; +import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springsource.ide.eclipse.commons.ui.UiUtil; + +/** + * URL control for property section composite + * + * @author Alex Boyko + * + */ +public class UrlPropertyControl extends AbstractBdePropertyControl { + + private final String label; + private final Class type; + private final Function getter; + + private Hyperlink url; + + public UrlPropertyControl(Class type, String label, Function getter) { + this.type = type; + this.label = label; + this.getter = getter; + } + + @Override + public void createControl(Composite composite, TabbedPropertySheetPage page) { + super.createControl(composite, page); + + page.getWidgetFactory().createLabel(composite, label).setLayoutData(GridDataFactory.fillDefaults().create()); //$NON-NLS-1$ + url = page.getWidgetFactory().createHyperlink(composite, getUrl(getBootDashElement()), SWT.NO_FOCUS); + url.setLayoutData(GridDataFactory.fillDefaults().align(SWT.BEGINNING, SWT.FILL).create()); + url.setEnabled(false); + url.addHyperlinkListener(new HyperlinkAdapter() { + @Override + public void linkActivated(HyperlinkEvent e) { + if (!url.getText().isEmpty()) { + UiUtil.openUrl(url.getText()); + } + } + }); + } + + @Override + public void refreshControl() { + BootDashElement element = getBootDashElement(); + if (url != null && !url.isDisposed()) { + String text = getUrl(element); + url.setText(text); + url.setEnabled(!text.isEmpty()); + int width = url.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; + GridData data = GridDataFactory.copyData(((GridData)url.getLayoutData())); + data.widthHint = width; + url.setLayoutData(data); + url.getParent().layout(); + } + } + + protected String getUrl(BootDashElement element) { + if (element!=null && type.isAssignableFrom(element.getClass())) { + @SuppressWarnings("unchecked") + String url = getter.apply((T)element); + if (url!=null) { + return url; + } + } + return ""; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashColumn.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashColumn.java new file mode 100644 index 000000000..d7c5acc1a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashColumn.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.views.sections; + +public class BootDashColumn { + + private final String ID; + + public BootDashColumn(String ID) { + this.ID = ID; + } + + @Override + public String toString() { + return ID; + } + + public static final BootDashColumn RUN_STATE_ICN = new BootDashColumn("RUN_STATE_ICN"); + public static final BootDashColumn INSTANCES = new BootDashColumn("INSTANCES"); + public static final BootDashColumn PROJECT = new BootDashColumn("PROJECT"); + public static final BootDashColumn NAME = new BootDashColumn("NAME"); + public static final BootDashColumn HOST = new BootDashColumn("HOST"); + public static final BootDashColumn LIVE_PORT = new BootDashColumn("LIVE_PORT"); + public static final BootDashColumn DEFAULT_PATH = new BootDashColumn("DEFAULT_PATH"); + public static final BootDashColumn TAGS = new BootDashColumn("TAGS"); + public static final BootDashColumn EXPOSED_URL = new BootDashColumn("EXPOSED_URL"); + public static final BootDashColumn DEVTOOLS = new BootDashColumn("DEVTOOLS"); + public static final BootDashColumn TREE_VIEWER_MAIN = new BootDashColumn("TREE_VIEWER_MAIN"); //this is a 'fake' column which corresponds to the single column shown in unified tree viewer. + public static final BootDashColumn PROGRESS = new BootDashColumn("PROGRESS"); +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashTreeContentProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashTreeContentProvider.java new file mode 100644 index 000000000..96b4a9567 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashTreeContentProvider.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.views.sections; + +import java.util.ArrayList; + +import org.apache.commons.lang3.ArrayUtils; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.ButtonModel; + +import com.google.common.collect.ImmutableSet; + +/** + * @author Kris De Volder + */ +public class BootDashTreeContentProvider implements ITreeContentProvider { + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object e) { + return getChildren(e); + } + + @Override + public Object[] getChildren(Object e) { + if (e instanceof BootDashViewModel) { + return ((BootDashViewModel) e).getSectionModels().getValue().toArray(); + } else if (e instanceof BootDashModel) { + BootDashModel model = ((BootDashModel) e); + ImmutableSet bdes = model.getElements().getValue(); + ImmutableSet buttons = model.getButtons().getValue(); + if (buttons.isEmpty()) { + //common special case, avoid extra intermediate collection creation + return bdes.toArray(); + } + ArrayList merged = new ArrayList<>(bdes.size()+buttons.size()); + merged.addAll(buttons); + merged.addAll(bdes); + return merged.toArray(); + } else if (e instanceof BootDashElement) { + return ((BootDashElement)e).getChildren().getValues().toArray(); + } + return null; + } + + @Override + public Object getParent(Object e) { + if (e instanceof BootDashElement) { + return ((BootDashElement) e).getParent(); + } else if (e instanceof BootDashModel) { + return ((BootDashModel) e).getViewModel(); + } + return null; + } + + @Override + public boolean hasChildren(Object e) { + return ArrayUtils.isNotEmpty(getChildren(e)); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashTreeLabelProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashTreeLabelProvider.java new file mode 100644 index 000000000..b6173d423 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashTreeLabelProvider.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.jface.viewers.ColumnViewer; +import org.springframework.ide.eclipse.boot.dash.views.BootDashCellLabelProvider; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +/** + * @author Kris De Volder + */ +public class BootDashTreeLabelProvider extends BootDashCellLabelProvider { + + public BootDashTreeLabelProvider(Stylers stylers, ColumnViewer tv) { + //TODO: refactor so we do not need this 'dummy' column? + super(tv, BootDashColumn.TREE_VIEWER_MAIN/*dummy*/, stylers); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashUnifiedTreeSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashUnifiedTreeSection.java new file mode 100644 index 000000000..fd665d1e6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/BootDashUnifiedTreeSection.java @@ -0,0 +1,780 @@ +/******************************************************************************* + * 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.views.sections; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.Set; + +import org.apache.commons.lang3.ArrayUtils; +import org.eclipse.core.runtime.Assert; +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.action.IMenuListener; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.preference.JFacePreferences; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.LocalSelectionTransfer; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.jface.viewers.ViewerFilter; +import org.eclipse.jface.viewers.ViewerSorter; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.DND; +import org.eclipse.swt.dnd.DragSourceAdapter; +import org.eclipse.swt.dnd.DragSourceEvent; +import org.eclipse.swt.dnd.DropTarget; +import org.eclipse.swt.dnd.DropTargetAdapter; +import org.eclipse.swt.dnd.DropTargetEvent; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.ui.PlatformUI; +import org.springframework.ide.eclipse.boot.dash.BootDashActivator; +import org.springframework.ide.eclipse.boot.dash.di.SimpleDIContext; +import org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels; +import org.springframework.ide.eclipse.boot.dash.liveprocess.LiveProcessCommandsExecutor; +import org.springframework.ide.eclipse.boot.dash.livexp.ElementwiseListener; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelectionSource; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ModelStateListener; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.ButtonModel; +import org.springframework.ide.eclipse.boot.dash.model.ModifiableModel; +import org.springframework.ide.eclipse.boot.dash.model.RunTarget; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +import org.springframework.ide.eclipse.boot.dash.util.HiddenElementsLabel; +import org.springframework.ide.eclipse.boot.dash.util.MenuUtil; +import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashAction; +import org.springframework.ide.eclipse.boot.dash.views.AddRunTargetAction; +import org.springframework.ide.eclipse.boot.dash.views.BootDashActions; +import org.springframework.ide.eclipse.boot.dash.views.RunStateAction; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +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.UIValueListener; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.PageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; +import org.springsource.ide.eclipse.commons.livexp.ui.util.ReflowUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Filter; +import org.springsource.ide.eclipse.commons.livexp.util.Log; +import org.springsource.ide.eclipse.commons.ui.UiUtil; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * Displays all runtargets and elements in a single 'unified' tree viewer. + * + * @author Kris De Volder + */ +public class BootDashUnifiedTreeSection extends PageSection implements MultiSelectionSource { + + private static final boolean DEBUG = false; + + private void debug(final String name, LiveExpression watchable) { + if (DEBUG) { + watchable.addListener(new ValueListener() { + public void gotValue(LiveExpression exp, T value) { + System.out.println(name +": "+ value); + } + }); + } + } + + protected static final Object[] NO_OBJECTS = new Object[0]; + + private MenuManager menuMgr; + private CustomTreeViewer tv; + private BootDashViewModel model; + private MultiSelection mixedSelection; // selection that may contain section or element nodes or both. + private MultiSelection selection; + private LiveExpression sectionSelection; + private BootDashActions actions; + private LiveExpression> searchFilterModel; + private Stylers stylers; + private final SimpleDIContext context; + + + private final IPropertyChangeListener THEME_LISTENER = new IPropertyChangeListener() { + + @Override + public void propertyChange(PropertyChangeEvent event) { + switch (event.getProperty()) { + case BootDashLabels.TEXT_DECORATION_COLOR_THEME: + case BootDashLabels.ALT_TEXT_DECORATION_COLOR_THEME: + case BootDashLabels.MUTED_TEXT_DECORATION_COLOR_THEME: + case JFacePreferences.HYPERLINK_COLOR: + if (!tv.getTree().isDisposed()) { + tv.refresh(true); + } + break; + } + } + }; + + public static class BootModelViewerSorter extends ViewerSorter { + + private final BootDashViewModel viewModel; + + public BootModelViewerSorter(BootDashViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public int compare(Viewer viewer, Object e1, Object e2) { + if (e1 instanceof BootDashModel && e2 instanceof BootDashModel) { + return this.viewModel.getModelComparator().compare((BootDashModel) e1, (BootDashModel) e2); + } else if (e1 instanceof BootDashElement && e2 instanceof BootDashElement) { + BootDashElement bde1 = (BootDashElement) e1; + BootDashElement bde2 = (BootDashElement) e2; + if (bde1.getBootDashModel()==bde2.getBootDashModel()) { + Comparator comparator = bde1.getBootDashModel().getElementComparator(); + if (comparator!=null) { + return comparator.compare(bde1, bde2); + } + } + } else if (e1 instanceof ButtonModel && e2 instanceof ButtonModel) { + return ((ButtonModel)e1).getLabel().compareTo(((ButtonModel)e2).getLabel()); + } else if (e1 instanceof ButtonModel) { + return -1; + } else if (e2 instanceof ButtonModel) { + return +1; + } + return super.compare(viewer, e1, e2); + } + } + + final private ValueListener> FILTER_LISTENER = new UIValueListener>() { + public void uiGotValue(LiveExpression> exp, Filter value) { + tv.refresh(); + final Tree t = tv.getTree(); + t.getDisplay().asyncExec(new Runnable() { + public void run() { + Composite parent = t.getParent(); + parent.layout(); + } + }); + } + }; + + final private ElementStateListener ELEMENT_STATE_LISTENER = new ElementStateListener() { + public void stateChanged(final BootDashElement e) { + Display.getDefault().asyncExec(new Runnable() { + public void run() { + if (tv != null && !tv.getControl().isDisposed()) { + //tv.update(e, null); + tv.refresh(e, true); + tv.getControl().redraw(); + } + } + }); + } + }; + + final private ModelStateListener MODEL_STATE_LISTENER = new ModelStateListener() { + public void stateChanged(final BootDashModel model) { + Display.getDefault().asyncExec(new Runnable() { + public void run() { + if (tv != null && !tv.getControl().isDisposed()) { + tv.refresh(); + /* + * TODO: ideally the above should do the repaint of + * the control's area where the tree item is + * located, but for some reason repaint doesn't + * happen. #refresh() didn't trigger the repaint either + */ + tv.getControl().redraw(); + } else { + model.removeModelStateListener(MODEL_STATE_LISTENER); + } + } + }); + } + }; + + final private ValueListener> RUN_TARGET_LISTENER = new UIValueListener>() { + protected void uiGotValue(LiveExpression> exp, ImmutableSet value) { + if (tv != null && !tv.getControl().isDisposed()) { + tv.refresh(); + } + } + }; + + @SuppressWarnings({"rawtypes", "unchecked"}) //Raw types, because the types are getting in the way. + //This is fine because we don't really care about the values here, so we don't really care about + //their types either. + private final ValueListener ELEMENTS_SET_LISTENER = new UIValueListener() { + protected void uiGotValue(LiveExpression exp, Object value) { + if (tv != null && !tv.getControl().isDisposed()) { + //TODO: refreshing the whole table is overkill, but is a bit tricky to figure out which BDM + // this set of elements belong to. If we did know then we could just refresh the node representing its section + // only. + tv.refresh(); + } else { + //This listener can't easily be removed because of the intermediary adapter that adds it to a numner of different + // things. So at least remove it when model remains chatty after view got disposed. + exp.removeListener(this); + } + } + }; + + /** + * Listener which adds element set listener to each section model. + */ + @SuppressWarnings("unchecked") + final private ValueListener> ELEMENTS_SET_LISTENER_ADAPTER = new ElementwiseListener() { + protected void added(LiveExpression> exp, BootDashModel e) { + e.getElements().addListener((ValueListener>) ELEMENTS_SET_LISTENER); + e.getButtons().addListener((ValueListener>) ELEMENTS_SET_LISTENER); + e.addModelStateListener(MODEL_STATE_LISTENER); + } + protected void removed(LiveExpression> exp, BootDashModel e) { + e.getElements().removeListener((ValueListener>) ELEMENTS_SET_LISTENER); + e.getButtons().removeListener((ValueListener>) ELEMENTS_SET_LISTENER); + e.removeModelStateListener(MODEL_STATE_LISTENER); + } + }; + + public static class CustomTreeViewer extends TreeViewer { + + private LiveVariable hiddenElementCount = new LiveVariable<>(0); + + public CustomTreeViewer(Composite page, int style) { + super(page, style); + } + + @Override + public void refresh(Object obj) { + super.refresh(obj); + // Every sub-tree refresh should update the hidden elements label + int totalElements = countChildren(getRoot()); + int filteredElements = countFilteredChildren(getRoot()); + hiddenElementCount.setValue(totalElements - filteredElements); + } + + private int countChildren(Object element) { + int count = 0; + for (Object o : getRawChildren(element)) { + count += 1 + countChildren(o); + } + return count; + } + + private int countFilteredChildren(Object element) { + int count = 0; + for (Object o : super.getFilteredChildren(element)) { + count += 1 + countFilteredChildren(o); + } + return count; + } + + } + + public BootDashUnifiedTreeSection(IPageWithSections owner, BootDashViewModel model, SimpleDIContext context) { + super(owner); + Assert.isNotNull(context); + context.assertDefinitionFor(UserInteractions.class); + this.context = context; + this.model = model; + this.searchFilterModel = model.getFilter(); + } + + @Override + public void createContents(Composite page) { + tv = new CustomTreeViewer(page, SWT.V_SCROLL | SWT.H_SCROLL | SWT.MULTI); + tv.setExpandPreCheckFilters(true); + tv.setContentProvider(new BootDashTreeContentProvider()); + tv.setSorter(new BootModelViewerSorter(this.model)); + tv.setInput(model); + tv.getTree().setLinesVisible(false); + + stylers = new Stylers(tv.getTree().getFont()); + tv.setLabelProvider(new BootDashTreeLabelProvider(stylers, tv)); + + ColumnViewerToolTipSupport.enableFor(tv); + + GridDataFactory.fillDefaults().grab(true, true).applyTo(tv.getTree()); + + new HiddenElementsLabel(page, tv.hiddenElementCount); + + tv.getControl().addControlListener(new ControlListener() { + public void controlResized(ControlEvent e) { + ReflowUtil.reflow(owner, tv.getControl()); + } + + public void controlMoved(ControlEvent e) { + } + }); + + actions = new BootDashActions(model, getElementSelection(), getSectionSelection(), context, LiveProcessCommandsExecutor.getDefault()); + hookContextMenu(); + + // Careful, either selection or tableviewer might be created first. + // in either case we must make sure the listener is added when *both* + // have been created. + if (selection != null) { + addViewerSelectionListener(); + } + + tv.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + if (selection != null) { + BootDashElement selected = selection.getSingle(); + if (selected != null) { + String url = selected.getUrl(); + if (url != null) { + UiUtil.openUrl(url); + } + } + else { + BootDashModel section = sectionSelection.getValue(); + section.performDoubleClickAction(ui()); + } + } + } + }); + tv.getTree().addMouseListener(new MouseAdapter() { + @Override + public void mouseDown(MouseEvent evt) { + Point point = new Point(evt.x, evt.y); + ViewerCell cell = tv.getCell(point); + if (cell!=null) { + Object element = cell.getElement(); + if (element instanceof ButtonModel) { + ButtonModel button = (ButtonModel) element; + Job job = new Job(button.getLabel()) { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + button.perform(ui()); + } catch (Exception e) { + Log.log(e); + } + return Status.OK_STATUS; + } + + }; + job.schedule(); + } + } + } + + }); + + model.getRunTargets().addListener(RUN_TARGET_LISTENER); + model.getSectionModels().addListener(ELEMENTS_SET_LISTENER_ADAPTER); + + model.addElementStateListener(ELEMENT_STATE_LISTENER); + + if (searchFilterModel != null) { + searchFilterModel.addListener(FILTER_LISTENER); + tv.addFilter(new ViewerFilter() { + @Override + public boolean select(Viewer viewer, Object parentElement, Object element) { + if (searchFilterModel.getValue() != null && element instanceof BootDashElement) { + return searchFilterModel.getValue().accept((BootDashElement) element); + } + return true; + } + }); + } + + tv.getTree().addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + model.removeElementStateListener(ELEMENT_STATE_LISTENER); + model.getRunTargets().removeListener(RUN_TARGET_LISTENER); + model.getSectionModels().removeListener(ELEMENTS_SET_LISTENER_ADAPTER); + for (BootDashModel m : model.getSectionModels().getValue()) { + m.removeModelStateListener(MODEL_STATE_LISTENER); + } + + if (searchFilterModel!=null) { + searchFilterModel.removeListener(FILTER_LISTENER); + } + + if (actions!=null) { + actions.dispose(); + actions = null; + } + if (stylers != null) { + stylers.dispose(); + stylers = null; + } + + PlatformUI.getWorkbench().getThemeManager().removePropertyChangeListener(THEME_LISTENER); + } + }); + + PlatformUI.getWorkbench().getThemeManager().addPropertyChangeListener(THEME_LISTENER); + + addDragSupport(tv); + addDropSupport(tv); + + } + + private LiveExpression getSectionSelection() { + if (sectionSelection==null) { + sectionSelection = getMixedSelection().toSingleSelection().filter(BootDashModel.class); + debug("sectionSelection", sectionSelection); + } + return sectionSelection; + } + + private synchronized MultiSelection getMixedSelection() { + if (mixedSelection==null) { + mixedSelection = MultiSelection.from(Object.class, new ObservableSet() { + @Override + protected ImmutableSet compute() { + if (tv!=null) { + ISelection s = tv.getSelection(); + if (s instanceof IStructuredSelection) { + Object[] elements = ((IStructuredSelection) s).toArray(); + return ImmutableSet.copyOf(elements); + } + } + return ImmutableSet.of(); + } + }); + debug("mixedSelection", mixedSelection.getElements()); + } + if (tv!=null) { + addViewerSelectionListener(); + } + return mixedSelection; + } + + + @Override + public synchronized MultiSelection getSelection() { + return getMixedSelection(); + } + + private synchronized MultiSelection getElementSelection() { + if (selection==null) { + selection = getMixedSelection().filter(BootDashElement.class); + debug("selection", selection.getElements()); + } + return selection; + } + + private void addViewerSelectionListener() { + tv.setSelection(new StructuredSelection(Arrays.asList(mixedSelection.getValue().toArray()))); + tv.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + mixedSelection.getElements().refresh(); + } + }); + } + + private void hookContextMenu() { + this.menuMgr = new MenuManager("#PopupMenu"); + menuMgr.setRemoveAllWhenShown(true); + menuMgr.addMenuListener(new IMenuListener() { + public void menuAboutToShow(IMenuManager manager) { + fillContextMenu(manager); + } + }); + Menu menu = menuMgr.createContextMenu(tv.getControl()); + tv.getControl().setMenu(menu); + } + + + private void fillContextMenu(IMenuManager manager) { + for (RunStateAction a : actions.getRunStateActions()) { + addVisible(manager, a); + } + addVisible(manager, actions.getOpenBrowserAction()); + addVisible(manager, actions.getOpenNgrokAdminUi()); + addVisible(manager, actions.getOpenConsoleAction()); + addVisible(manager, actions.getOpenInPackageExplorerAction()); + addVisible(manager, actions.getShowPropertiesViewAction()); + MenuUtil.addDynamicSubmenu(manager, actions.getLiveDataConnectionManagement()); + + manager.add(new Separator()); + + addVisible(manager, actions.getOpenConfigAction()); + addVisible(manager, actions.getDuplicateConfigAction()); + addVisible(manager, actions.getDeleteConfigsAction()); + + manager.add(new Separator()); + + addVisible(manager, actions.getExposeRunAppAction()); + addVisible(manager, actions.getExposeDebugAppAction()); + addSubmenu(manager, "Deploy and Run On...", BootDashActivator.getImageDescriptor("icons/run-on-cloud.png"), actions.getRunOnTargetActions()); + addSubmenu(manager, "Deploy and Debug On...", BootDashActivator.getImageDescriptor("icons/debug-on-cloud.png"), actions.getDebugOnTargetActions()); + manager.add(new Separator()); + + for (AddRunTargetAction a : actions.getAddRunTargetActions()) { + addVisible(manager, a); + } + manager.add(new Separator()); + + addVisible(manager, actions.getConnectAction()); + addVisible(manager, actions.getRemoveRunTargetAction()); + addVisible(manager, actions.getRefreshRunTargetAction()); + addVisible(manager, actions.getDeleteAppsAction()); + addVisible(manager, actions.getEnableDevtoolsAction()); + addVisible(manager, actions.getRestartDevtoolsClientAction()); + for (IAction a : actions.getInjectedActions(AbstractBootDashAction.Location.CONTEXT_MENU)) { + addVisible(manager, a); + } + + manager.add(new Separator()); + + ImmutableList.Builder customizeActions = ImmutableList.builder(); + customizeActions.add(actions.getCustomizeTargetLabelAction()); + for (IAction a : actions.getInjectedActions(AbstractBootDashAction.Location.CUSTOMIZE_MENU)) { + customizeActions.add(a); + } + addSubmenu(manager, "Customize...", null, customizeActions.build()); + } + + /** + * Adds a submenu containing a given list of actions. The menu is only added if + * there is at least one visible action in the list. + * @param imageDescriptor + */ + private void addSubmenu(IMenuManager parent, String label, ImageDescriptor imageDescriptor, ImmutableList actions) { + if (actions!=null && !actions.isEmpty()) { + boolean notEmpty = false; + MenuManager submenu = new MenuManager(label); + for (IAction a : actions) { + notEmpty |= addVisible(submenu, a); + } + if (notEmpty) { + submenu.setImageDescriptor(imageDescriptor); + parent.add(submenu); + } + } + } + + public static boolean addVisible(IMenuManager manager, IAction a) { + if (a!=null && isVisible(a)) { + manager.add(a); + return true; + } + return false; + } + + public static boolean isVisible(IAction a) { + if (a instanceof AbstractBootDashAction) { + return ((AbstractBootDashAction) a).isVisible(); + } + return true; + } + + private void addDragSupport(final TreeViewer viewer) { + int ops = DND.DROP_COPY; + + final Transfer[] transfers = new Transfer[] { LocalSelectionTransfer.getTransfer() }; + + DragSourceAdapter listener = new DragSourceAdapter() { + +// @Override +// public void dragSetData(DragSourceEvent event) { +// IStructuredSelection selection = (IStructuredSelection) viewer.getSelection(); +// event.data = selection.getFirstElement(); +// LocalSelectionTransfer.getTransfer().setSelection(selection); +// } +// +// @Override +// public void dragStart(DragSourceEvent event) { +// if (event.detail == DND.DROP_NONE || event.detail == DND.DROP_DEFAULT) { +// event.detail = DND.DROP_COPY; +// } +// dragSetData(event); +// } + + @Override + public void dragSetData(DragSourceEvent event) { + Set selection = getElementSelection().getValue(); + BootDashElement[] elements = selection.toArray(new BootDashElement[selection.size()]); + LocalSelectionTransfer.getTransfer().setSelection(new StructuredSelection(elements)); + event.detail = DND.DROP_COPY; + } + + @Override + public void dragStart(DragSourceEvent event) { + if (!canDeploySelection(getElementSelection().getValue())) { + event.doit = false; + } else { + dragSetData(event); + } + } + }; + viewer.addDragSupport(ops, transfers, listener); + } + + private void addDropSupport(final TreeViewer tv) { + int ops = DND.DROP_COPY; + final LocalSelectionTransfer transfer = LocalSelectionTransfer.getTransfer(); + Transfer[] transfers = new Transfer[] { LocalSelectionTransfer.getTransfer() }; + DropTarget dropTarget = new DropTarget(tv.getTree(), ops); + dropTarget.setTransfer(transfers); + dropTarget.addDropListener(new DropTargetAdapter() { + @Override + public void dragEnter(DropTargetEvent event) { + checkDropable(event); + } + + @Override + public void dragOver(DropTargetEvent event) { + checkDropable(event); + event.feedback = DND.FEEDBACK_SELECT | DND.FEEDBACK_SCROLL; + } + + @Override + public void dropAccept(DropTargetEvent event) { + checkDropable(event); + } + + private void checkDropable(DropTargetEvent event) { + if (canDrop(event)) { + event.detail = DND.DROP_COPY & event.operations; + } else { + event.detail = DND.DROP_NONE; + } + } + + private boolean canDrop(DropTargetEvent event) { + BootDashModel droppedOn = getDropTarget(event); + if (droppedOn!=null && droppedOn instanceof ModifiableModel) { + ModifiableModel target = (ModifiableModel) droppedOn; + if (transfer.isSupportedType(event.currentDataType)) { + Object[] elements = getDraggedElements(); + if (ArrayUtils.isNotEmpty(elements) && target.canBeAdded(Arrays.asList(elements))) { + return true; + } + } + } + return false; + } + + + /** + * Determines which BootDashModel a droptarget event represents (i.e. what thing + * are we dropping or dragging onto? + */ + private BootDashModel getDropTarget(DropTargetEvent event) { + Point loc = tv.getTree().toControl(new Point(event.x, event.y)); + ViewerCell cell = tv.getCell(loc); + if (cell!=null) { + Object el = cell.getElement(); + if (el instanceof BootDashModel) { + return (BootDashModel) el; + } + } + //Not a valid place to drop + return null; + } + + @Override + public void drop(DropTargetEvent event) { + if (canDrop(event)) { + BootDashModel model = getDropTarget(event); + final Object[] elements = getDraggedElements(); + if (model instanceof ModifiableModel) { + final ModifiableModel modifiableModel = (ModifiableModel) model; + Job job = new Job("Performing deployment to " + model.getRunTarget().getName()) { + + @Override + protected IStatus run(IProgressMonitor monitor) { + if (modifiableModel != null && selection != null) { + try { + modifiableModel.add(Arrays.asList(elements)); + + } catch (Exception e) { + ui().errorPopup("Failed to Add Element", e.getMessage()); + } + } + return Status.OK_STATUS; + } + + }; + job.schedule(); + } + } + super.drop(event); + } + + private Object[] getDraggedElements() { + ISelection sel = transfer.getSelection(); + if (sel instanceof IStructuredSelection) { + return ((IStructuredSelection)sel).toArray(); + } + return NO_OBJECTS; + } + + }); + } + + private boolean canDeploySelection(Set selection) { + if (selection.isEmpty()) { + //Careful... don't return 'true' if nothing is selected. + return false; + } + for (BootDashElement e : selection) { + if (!e.getBootDashModel().getRunTarget().canDeployAppsFrom()) { + return false; + } + } + return true; + } + + @Override + public LiveExpression getValidator() { + return Validator.OK; + } + + private UserInteractions ui() { + return context.getBean(UserInteractions.class); + } + + public MenuManager getMenuMgr() { + return menuMgr; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/CursorProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/CursorProvider.java new file mode 100644 index 000000000..340565269 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/CursorProvider.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views.sections; + +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.swt.graphics.Cursor; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +/** + * Same idea as a 'LabelProvider' but provides mouse cursor for viewer cells. + *

+ * Note that, typcually, a cursor provider might need to allocate Cursor objects. + * In which case it is also responsible for disposing them. In that case, + * it should also implement {@link Disposable}. + * + * @author Kris De Volder + */ +public interface CursorProvider { + + Cursor getCursor(ViewerCell cell); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/CursorProviders.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/CursorProviders.java new file mode 100644 index 000000000..54d527045 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/CursorProviders.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views.sections; + +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Cursor; + +public class CursorProviders { + + /** + * A mouse cursor provider that shows the hand cursor, always. + */ + public static final CursorProvider HAND_CURSOR = forStyle(SWT.CURSOR_HAND); + + /** + * Create a cursor provider for given + */ + public static CursorProvider forStyle(final int cursorStyle) { + return new CursorProvider() { + + private Cursor cursor; + + @Override + public Cursor getCursor(ViewerCell cell) { + if (cursor==null) { + cursor = new Cursor(cell.getControl().getDisplay(), cursorStyle); + } + return cursor; + } + }; + }; + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/DefaultPathEditorSupport.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/DefaultPathEditorSupport.java new file mode 100644 index 000000000..abf92eca0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/DefaultPathEditorSupport.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.jface.fieldassist.ContentProposalAdapter; +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.EditingSupport; +import org.eclipse.jface.viewers.TableViewer; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.launch.util.TextCellEditorWithContentProposal; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +public class DefaultPathEditorSupport extends EditingSupport { + + private CellEditor editor; + + public DefaultPathEditorSupport(TableViewer tableViewer, LiveExpression input, Stylers stylers) { + super(tableViewer); + IContentProposalProvider proposalProvider = + // new SimpleContentProposalProvider(new String[] {"red", "green", "blue"}); + new RequestMappingContentProposalProvider(input); + this.editor = new TextCellEditorWithContentProposal(tableViewer.getTable(), + proposalProvider, UIUtils.CTRL_SPACE, UIUtils.PATH_CA_AUTO_ACTIVATION_CHARS + ).setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE); + + +// this.editor = new TextCellEditor(tableViewer.getTable()); + } + + @Override + protected CellEditor getCellEditor(Object element) { + return editor; + } + + @Override + protected boolean canEdit(Object element) { + return element instanceof BootDashElement; + } + + @Override + protected Object getValue(Object element) { + if (element instanceof BootDashElement) { + String path = ((BootDashElement) element).getDefaultRequestMappingPath(); + return path==null?"":path; + } + return "?huh?"; + } + + @Override + protected void setValue(Object element, Object value) { + if (value instanceof String && element instanceof BootDashElement) { + ((BootDashElement)element).setDefaultRequestMappingPath((String) value); + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/DynamicCompositeSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/DynamicCompositeSection.java new file mode 100644 index 000000000..2f2a40ccd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/DynamicCompositeSection.java @@ -0,0 +1,195 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.springframework.ide.eclipse.boot.dash.livexp.DelegatingLiveSet; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelectionSource; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +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.ui.IPageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.PageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.util.ReflowUtil; + +public class DynamicCompositeSection extends PageSection implements MultiSelectionSource, Disposable { + + public static interface SectionFactory { + IPageSection create(M model); + } + + /** + * A dynamically changing collection of models. When this changes the DynamicComposite will react by + * creating or deleting sections corresponding to the corresponding added/deleted models. + */ + private LiveExpression> models; + + /** + * Keeps track of sections per model. + */ + private Map sectionsMap = new LinkedHashMap<>(); + + /** + * Keeps track of the selected elements + */ + private DelegatingLiveSet elements; + private MultiSelection selection; //Because of the dynamic nature, we can't really know what kinds of selections will get added + // So we must assume any kinds of object may appear. + private Composite composite = null; //Set once UI is created + + private SectionFactory sectionFactory; + + private ValueListener> modelListener = new ValueListener>() { + public void gotValue(LiveExpression> exp, Set value) { + updateSections(); + } + }; + + private Class selectionType = Object.class; + + private class SubSection { + Collection ui; + IPageSection section; + } + + public DynamicCompositeSection(IPageWithSections owner, LiveExpression> models, SectionFactory sectionFactory, Class selectionType) { + super(owner); + this.models = models; + DelegatingLiveSet _elements = new DelegatingLiveSet<>(); + this.elements = _elements; + this.sectionFactory = sectionFactory; + this.selectionType = selectionType; + this.selection = MultiSelection.from(selectionType, _elements); + } + + @Override + public MultiSelection getSelection() { + return selection; + } + + @Override + public LiveExpression getValidator() { + return OK_VALIDATOR; + } + + @Override + public void createContents(Composite page) { +// composite = new Composite(page, SWT.NONE); +// Layout l = new GridLayout(); +// composite.setLayout(l); +// GridDataFactory.fillDefaults().grab(true, true).applyTo(composite); + composite = page; + + models.addListener(modelListener); + } + + /** + * Called when we need to update the sections based on the models + */ + private synchronized void updateSections() { + Set currentModels = models.getValue(); + + //Missing: current models for which we have no section + Set missing = new LinkedHashSet<>(currentModels); + missing.removeAll(sectionsMap.keySet()); + + //Extra: current sections for which there is no more model + Set extra = new HashSet<>(); + extra.addAll(sectionsMap.keySet()); + extra.removeAll(currentModels); + + for (M m : extra) { + deleteSectionFor(m); + } + + for (M m : missing) { + createSectionFor(m); + } + + boolean dirty = !missing.isEmpty() || !extra.isEmpty(); + if (dirty) { + ReflowUtil.reflow(owner, composite); + updateSelectionDelegate(); + } + } + + @SuppressWarnings("unchecked") + private void updateSelectionDelegate() { + Class selectionType = (Class) this.selectionType; + MultiSelection newSelection = MultiSelection.empty(selectionType); + for (SubSection s : sectionsMap.values()) { + if (s.section instanceof MultiSelectionSource) { + newSelection = combineSelections( + newSelection, + ((MultiSelectionSource)s.section).getSelection().filter(selectionType) + ); + } + } + @SuppressWarnings("rawtypes") + ObservableSet newElements = newSelection.getElements(); + elements.setDelegate(newElements); + } + + protected MultiSelection combineSelections(MultiSelection a, MultiSelection b) { + return MultiSelection.union(a, b); + } + + private void createSectionFor(M m) { + Set oldWidgets = new HashSet<>(Arrays.asList(composite.getChildren())); + SubSection s = new SubSection(); + s.section = sectionFactory.create(m); + s.section.createContents(composite); + s.ui = new HashSet<>(Arrays.asList(composite.getChildren())); + s.ui.removeAll(oldWidgets); + sectionsMap.put(m, s); + } + + private void deleteSectionFor(M m) { + SubSection s = sectionsMap.get(m); + sectionsMap.remove(m); + if (s.section instanceof Disposable) { + ((Disposable) s.section).dispose(); + } + if (s.ui!=null) { + for (Control widget : s.ui) { + widget.dispose(); + } + } + } + + @Override + public void dispose() { + if (modelListener!=null) { + models.removeListener(modelListener); + } + if (sectionsMap!=null) { + for (SubSection s : sectionsMap.values()) { + if (s.section instanceof Disposable) { + ((Disposable)s.section).dispose(); + } + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/DynamicSubMenuSupplier.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/DynamicSubMenuSupplier.java new file mode 100644 index 000000000..df68762f4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/DynamicSubMenuSupplier.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 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.views.sections; + +import java.util.List; + +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.resource.ImageDescriptor; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +public interface DynamicSubMenuSupplier { + String getLabel(); + List getActions(); + default ImageDescriptor getImageDescriptor() { return null; } + default ImageDescriptor getDisabledImageDescriptor() { return null; } + + default boolean isVisible() { return true; } + default LiveExpression isEnabled() { return LiveExpression.constant(true); } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/EditingSupportFactory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/EditingSupportFactory.java new file mode 100644 index 000000000..5adc2f4fb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/EditingSupportFactory.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.jface.viewers.EditingSupport; +import org.eclipse.jface.viewers.TableViewer; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +/** + * Factory for creating table cell {@link EditingSupport} instance + * + * @author Alex Boyko + * + */ +public interface EditingSupportFactory { + + EditingSupport createEditingSupport(TableViewer viewer, LiveExpression selection, BootDashViewModel model, Stylers stylers); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/ExpandableSectionWithSelection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/ExpandableSectionWithSelection.java new file mode 100644 index 000000000..223ed9097 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/ExpandableSectionWithSelection.java @@ -0,0 +1,194 @@ +///******************************************************************************* +// * Copyright (c) 2015 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.views.sections; +// +//import java.util.Collections; +//import java.util.Set; +// +//import org.eclipse.jface.layout.GridDataFactory; +//import org.eclipse.jface.util.LocalSelectionTransfer; +//import org.eclipse.swt.SWT; +//import org.eclipse.swt.dnd.DND; +//import org.eclipse.swt.dnd.DropTarget; +//import org.eclipse.swt.dnd.Transfer; +//import org.eclipse.swt.layout.FillLayout; +//import org.eclipse.swt.layout.GridLayout; +//import org.eclipse.swt.widgets.Composite; +//import org.eclipse.swt.widgets.Control; +//import org.eclipse.ui.forms.events.ExpansionEvent; +//import org.eclipse.ui.forms.events.IExpansionListener; +//import org.eclipse.ui.forms.widgets.ExpandableComposite; +//import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +//import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelectionSource; +//import org.springframework.ide.eclipse.boot.dash.livexp.ObservableSet; +//import org.springframework.ide.eclipse.boot.dash.livexp.ui.ReflowUtil; +//import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +//import org.springframework.ide.eclipse.boot.dash.model.ModifiableModel; +//import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; +//import org.springframework.ide.eclipse.boot.dash.views.ViewStyler; +//import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +//import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +//import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +//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.ui.IPageWithSections; +//import org.springsource.ide.eclipse.commons.livexp.ui.PageSection; +// +///** +// * Section containing an ExpandableComposite that contains another +// * section which is shown/hidden inside the expandable composite. +// *

+// * The contained page section is assumed to be a SelectionSource +// * and so the expandable composite is as well. The 'selection' in +// * the child are propagated as the selection of the composite, +// * but only when the composite is in the 'expanded' state. +// * In non-expanded state, the composite propagates an empty +// * selection instead. +// *

+// * Note: This has not been used in many contexts and may not +// * be re-usable as is in contexts where it hasn't been tested. +// * The component is somewhat fiddly w.r.t. how parent composite +// * need to reflow their layout when this element is +// * expanded/collapsed. +// * +// * @author Kris De Volder +// */ +//public class ExpandableSectionWithSelection extends PageSection implements MultiSelectionSource, Disposable { +// +// private MultiSelectionSource child; +// private String title; +// private LiveVariable expansionState = new LiveVariable(true); +// private MultiSelection selection; +// private ViewStyler viewStyler; +// private BootDashModel model; +// private UserInteractions ui; +// private DropTarget dropTarget; +// +// public ExpandableSectionWithSelection(IPageWithSections owner, String title, MultiSelectionSource expandableContent, ViewStyler viewStyler, BootDashModel model, UserInteractions ui) { +// super(owner); +// this.title = title; +// this.child = expandableContent; +// this.selection = null; +// this.viewStyler = viewStyler; +// this.model = model; +// this.ui = ui; +// } +// +// protected MultiSelection createSelection() { +// if (child instanceof MultiSelectionSource) { +// @SuppressWarnings("unchecked") +// final MultiSelection childSelection = (MultiSelection) child.getSelection(); +// Class selectionType = childSelection.getElementType(); +// MultiSelection sel = MultiSelection.from(selectionType, +// new ObservableSet() { +// { +// this.dependsOn(expansionState); +// this.dependsOn(childSelection.getElements()); +// } +// @Override +// protected Set compute() { +// boolean isExpanded = expansionState.getValue(); +// if (isExpanded) { +// return childSelection.getValue(); +// } +// return Collections.emptySet(); +// } +// } +// ); +// return sel; +// } else { +// //child is not a selection source +// return MultiSelection.empty(Object.class); +// // ^^^^^ Object is wrong, the type we'd want here is the type of 'null'. I.e. a type that can +// // be assigned to any reference. But I don't beleave there is a java.lang.Class representing that. +// } +// } +// +// @Override +// public LiveExpression getValidator() { +// return OK_VALIDATOR; +// } +// +// private void addDropSupport(Composite composite) { +// +// if (model instanceof ModifiableModel && model.getRunTarget().canDeployAppsTo()) { +// int ops = DND.DROP_COPY | DND.DROP_LINK | DND.DROP_DEFAULT; +// Transfer[] transfers = new Transfer[] { LocalSelectionTransfer.getTransfer() }; +// +// dropTarget = new DropTarget(composite, ops); +// dropTarget.setTransfer(transfers); +// dropTarget.addDropListener(new ModelDropListener(model, ui)); +// } +// } +// +// @Override +// public void createContents(final Composite page) { +// final ExpandableComposite comp = new ExpandableComposite(page, SWT.NONE); +// +// addDropSupport(comp); +// +// GridDataFactory.fillDefaults().grab(true, false).applyTo(comp); +// comp.setText(title); +// comp.setLayout(new FillLayout()); +// comp.addExpansionListener(new IExpansionListener() { +// public void expansionStateChanging(ExpansionEvent e) { +// } +// @Override +// public void expansionStateChanged(ExpansionEvent e) { +// expansionState.setValue(comp.isExpanded()); +// ReflowUtil.reflow(owner, comp); +// } +// }); +// expansionState.addListener(new ValueListener() { +// public void gotValue(LiveExpression exp, Boolean value) { +// if (value!=null && comp!=null && !comp.isDisposed()) { +// boolean newState = value; +// boolean currentState = comp.isExpanded(); +// if (currentState!=newState) { +// comp.setExpanded(newState); +// } +// } +// } +// }); +// +// Composite client = new Composite(comp, SWT.NONE); +// client.setLayout(new GridLayout()); +// +// viewStyler.applyBackground(new Control[]{comp, client}); +// +// child.createContents(client); +// comp.setClient(client); +// } +// +// public LiveVariable getExpansionState() { +// return expansionState; +// } +// +// @Override +// public synchronized MultiSelection getSelection() { +// if (selection==null) { +// selection = createSelection(); +// } +// return selection; +// } +// +// @Override +// public void dispose() { +// if (child!=null) { +// if (child instanceof Disposable) { +// ((Disposable) child).dispose(); +// } +// child = null; +// } +// if (dropTarget != null && !dropTarget.isDisposed()) { +// dropTarget.dispose(); +// } +// }} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/ModelDropListener.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/ModelDropListener.java new file mode 100644 index 000000000..b24b157ad --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/ModelDropListener.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import java.util.Arrays; + +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.util.LocalSelectionTransfer; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.swt.dnd.DND; +import org.eclipse.swt.dnd.DropTargetAdapter; +import org.eclipse.swt.dnd.DropTargetEvent; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.ModifiableModel; +import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; + +public class ModelDropListener extends DropTargetAdapter { + + private final UserInteractions ui; + private final ModifiableModel modifiableModel; + private final BootDashModel model; + + private IStructuredSelection selection; + private Object target; + private boolean canDrop = false; + private int overrideDropOp = DND.DROP_COPY; + + public ModelDropListener(BootDashModel model, UserInteractions ui) { + this.ui = ui; + this.modifiableModel = model instanceof ModifiableModel ? (ModifiableModel) model : null; + this.model = model; + } + + @Override + public void drop(DropTargetEvent event) { + if (canDrop) { + Job job = new Job("Performing deployment to " + model.getRunTarget().getName()) { + + @Override + protected IStatus run(IProgressMonitor monitor) { + if (modifiableModel != null && selection != null) { + Object[] selectionObjs = selection.toArray(); + try { + modifiableModel.add(Arrays.asList(selectionObjs)); + + } catch (Exception e) { + ui.errorPopup("Failed to Add Element", e.getMessage()); + } + } + return Status.OK_STATUS; + } + + }; + job.schedule(); + } + + super.drop(event); + } + + + @Override + public void dropAccept(DropTargetEvent event) { + + int operations = event.operations; + canDrop = false; + if (modifiableModel != null && (operations == DND.DROP_COPY || operations == DND.DROP_DEFAULT || overrideDropOp == DND.DROP_COPY)) { + if (LocalSelectionTransfer.getTransfer().isSupportedType(event.currentDataType)) { + selection = (IStructuredSelection) LocalSelectionTransfer.getTransfer().getSelection(); + Object[] selectionObjs = selection.toArray(); + + if (selectionObjs != null) { + canDrop = modifiableModel.canBeAdded(Arrays.asList(selectionObjs)); + } + } + } + super.dropAccept(event); + } + + @Override + public void dragEnter(DropTargetEvent event) { + this.overrideDropOp = event.operations; + if (event.detail == DND.DROP_DEFAULT || event.detail == DND.DROP_NONE) { + event.detail = DND.DROP_COPY; + this.overrideDropOp = DND.DROP_COPY; + } + super.dragEnter(event); + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/RequestMappingContentProposalProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/RequestMappingContentProposalProvider.java new file mode 100644 index 000000000..6051d2c28 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/RequestMappingContentProposalProvider.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.dash.views.sections; + +import java.util.LinkedHashSet; + +import org.eclipse.jface.fieldassist.IContentProposal; +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping; +import org.springframework.ide.eclipse.editor.support.util.FuzzyMatcher; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; + +import com.google.common.collect.ImmutableList; + +public class RequestMappingContentProposalProvider implements IContentProposalProvider { + + private static final IContentProposal[] NO_PROPOSALS = {}; + + private LiveExpression input; + + public RequestMappingContentProposalProvider(LiveExpression input) { + this.input = input; + } + + @Override + public IContentProposal[] getProposals(String contents, int position) { + BootDashElement bde = input.getValue(); + if (bde!=null) { + ImmutableList rms = bde.getLiveRequestMappings().orElse(null); + if (rms!=null && !rms.isEmpty()) { + LinkedHashSet matches = new LinkedHashSet<>(rms.size()); + for (RequestMapping rm : rms) { + String path = rm.getPath(); + if (FuzzyMatcher.matchScore(contents, path)!=0) { + matches.add(path); + } + } + IContentProposal[] proposals = new IContentProposal[matches.size()]; + int i = 0; + for (String m : matches) { + proposals[i++] = simpleProposal(m); + } + return proposals; + } + } + return NO_PROPOSALS; + } + + private IContentProposal simpleProposal(final String path) { + return new IContentProposal() { + + @Override + public String getLabel() { + return path; + } + + @Override + public String getDescription() { + return null; + } + + @Override + public int getCursorPosition() { + return path.length(); + } + + @Override + public String getContent() { + return path; + } + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/SashSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/SashSection.java new file mode 100644 index 000000000..7c37c556b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/SashSection.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelectionSource; +import org.springsource.ide.eclipse.commons.livexp.core.CompositeValidator; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.PageSection; + +/** + * A SashSection is a composite consisting out of two resizable subsection, + * with a dragable divider in the middle. + * + * @author Kris De Volder + */ +public class SashSection extends PageSection implements Disposable, MultiSelectionSource { + + //TODO: The sash is always aligning view in a column. Generalize this to + // support both horizontal and vertical orientations. + + public SashSection(IPageWithSections owner, IPageSection top, IPageSection bottom) { + super(owner); + this.top = top; + this.bottom = bottom; + } + + private IPageSection top; + private IPageSection bottom; + private int topWeight = 70; + private int bottomWeight = 30; + + private CompositeValidator validator; + + private SashForm sashForm; + private Composite topComposite; + private Composite bottomComposite; + private int sashWidth = 8; + private MultiSelection selection; + + @Override + public synchronized LiveExpression getValidator() { + if (validator==null) { + validator = new CompositeValidator(); + validator.addChild(top.getValidator()); + validator.addChild(bottom.getValidator()); + } + return validator; + } + + @Override + public void createContents(Composite page) { + sashForm = createSashForm(page); + GridDataFactory.fillDefaults().grab(true, true).applyTo(sashForm); + sashForm.setLayout(new FillLayout()); + + this.topComposite = createChildComposite(sashForm); + top.createContents(topComposite); +// topComposite.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_GRAY)); + this.bottomComposite = createChildComposite(sashForm); + bottom.createContents(bottomComposite); + sashForm.setWeights(new int[] {topWeight, bottomWeight}); +// bottomComposite.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_GRAY)); + } + + protected SashForm createSashForm(Composite page) { + SashForm sf = new SashForm(page, SWT.VERTICAL); + sf.setSashWidth(getSashWidth()); + return sf; + } + + protected Composite createChildComposite(SashForm sashForm) { + Composite child = new Composite(sashForm, SWT.NONE); + child.setLayout(GridLayoutFactory.fillDefaults().create()); + return child; + } + + @Override + public void dispose() { + if (top!=null) { + if (top instanceof Disposable) { + ((Disposable) top).dispose(); + } + top = null; + } + if (bottom!=null) { + if (bottom instanceof Disposable) { + ((Disposable) bottom).dispose(); + } + bottom = null; + } + } + + public int getTopWeight() { + return topWeight; + } + + public void setTopWeight(int topWeight) { + this.topWeight = topWeight; + } + + public int getBottomWeight() { + return bottomWeight; + } + + public void setBottomWeight(int bottomWeight) { + this.bottomWeight = bottomWeight; + } + + public int getSashWidth() { + return sashWidth; + } + + public void setSashWidth(int sashWidth) { + this.sashWidth = sashWidth; + } + + @Override + public synchronized MultiSelection getSelection() { + //for now we keep it simple and only propagate selection from the 'dominant' child (i.e. the one at the top). + if (selection==null) { + if (top instanceof MultiSelectionSource) { + selection = ((MultiSelectionSource) top).getSelection(); + } else { + selection = MultiSelection.empty(Object.class); + } + } + return selection; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/Scroller.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/Scroller.java new file mode 100644 index 000000000..ae5791c53 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/Scroller.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.widgets.SharedScrolledComposite; +import org.springsource.ide.eclipse.commons.livexp.ui.Reflowable; + +public class Scroller extends SharedScrolledComposite implements Reflowable { + + public Scroller(Composite parent) { + this(parent, SWT.V_SCROLL | SWT.H_SCROLL); + } + + public Scroller(Composite parent, int style) { + super(parent, style); + + setFont(parent.getFont()); + +// fToolkit= JavaPlugin.getDefault().getDialogsFormToolkit(); + + setExpandHorizontal(true); + setExpandVertical(true); + + Composite body= new Composite(this, SWT.NONE); + body.setFont(parent.getFont()); + setContent(body); + } + + public Composite getBody() { + return (Composite) getContent(); + } + + @Override + public boolean reflow() { + reflow(true); + return true; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/ScrollerSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/ScrollerSection.java new file mode 100644 index 000000000..14163a906 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/ScrollerSection.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; +import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelectionSource; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.PageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.ViewStyler; + +/** + * Sections that wraps scrollbar support around another section. + * + * @author Kris De Volder + */ +public class ScrollerSection extends PageSection implements Disposable, MultiSelectionSource { + + private ViewStyler viewStyler; + + public ScrollerSection(IPageWithSections owner, IPageSection scolledContent, ViewStyler viewStyler) { + super(owner); + this.scolledContent = scolledContent; + this.viewStyler = viewStyler; + } + + private Scroller scroller; + private IPageSection scolledContent; + private MultiSelection selection; + + @Override + public void dispose() { + if (scolledContent!=null) { + if (scolledContent instanceof Disposable) { + ((Disposable) scolledContent).dispose(); + } + scolledContent = null; + } + } + + @Override + public LiveExpression getValidator() { + return scolledContent.getValidator(); + } + + @Override + public void createContents(Composite page) { + scroller = new Scroller(page); + GridDataFactory.fillDefaults().grab(true, true).applyTo(scroller); + Composite body = scroller.getBody(); + viewStyler.applyBackground(new Control[]{body}); + body.setLayout(GridLayoutFactory.swtDefaults().create()); + scolledContent.createContents(body); + } + + @Override + public synchronized MultiSelection getSelection() { + if (selection==null) { + if (scolledContent instanceof MultiSelectionSource) { + selection = ((MultiSelectionSource) scolledContent).getSelection(); + } else { + selection = MultiSelection.empty(Object.class); + } + } + return selection; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/StyledTextCellEditor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/StyledTextCellEditor.java new file mode 100644 index 000000000..2b3b2d8dd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/StyledTextCellEditor.java @@ -0,0 +1,563 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import java.text.MessageFormat; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.bindings.keys.KeyStroke; +import org.eclipse.jface.fieldassist.ContentProposalAdapter; +import org.eclipse.jface.fieldassist.IContentProposalListener2; +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.FocusAdapter; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.TraverseEvent; +import org.eclipse.swt.events.TraverseListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +public abstract class StyledTextCellEditor extends CellEditor { + + private ContentProposalAdapter contentProposalAdapter; + private boolean popupOpen = false; // true, iff popup is currently open + + /** + * The text control; initially null. + */ + protected StyledText text; + + private ModifyListener modifyListener; + + /** + * State information for updating action enablement + */ + private boolean isSelection = false; + + private boolean isDeleteable = false; + + private boolean isSelectable = false; + + /** + * Default TextCellEditor style + * specify no borders on text widget as cell outline in table already + * provides the look of a border. + */ + private static final int defaultStyle = SWT.SINGLE; + + /** + * Creates a new text string cell editor with no control + * The cell editor value is the string itself, which is initially the empty + * string. Initially, the cell editor has no cell validator. + * + * @since 2.1 + */ + public StyledTextCellEditor() { + setStyle(defaultStyle); + } + + /** + * Creates a new text string cell editor parented under the given control. + * The cell editor value is the string itself, which is initially the empty string. + * Initially, the cell editor has no cell validator. + * + * @param parent the parent control + */ + public StyledTextCellEditor(Composite parent) { + this(parent, defaultStyle); + } + + /** + * Creates a new text string cell editor parented under the given control. + * The cell editor value is the string itself, which is initially the empty string. + * Initially, the cell editor has no cell validator. + * + * @param parent the parent control + * @param style the style bits + * @since 2.1 + */ + public StyledTextCellEditor(Composite parent, int style) { + super(parent, style); + } + + public StyledTextCellEditor(Composite parent, IContentProposalProvider contentProposalProvider, + KeyStroke keyStroke, char[] autoActivationCharacters) { + this(parent); + + enableContentProposal(contentProposalProvider, keyStroke, autoActivationCharacters); + } + + public StyledTextCellEditor setProposalAcceptanceStyle(int acceptance) { + contentProposalAdapter.setProposalAcceptanceStyle(acceptance); + return this; + } + + private void enableContentProposal(IContentProposalProvider contentProposalProvider, KeyStroke keyStroke, + char[] autoActivationCharacters) { + contentProposalAdapter = new ContentProposalAdapter(text, new StyledTextContentAdapter(), + contentProposalProvider, keyStroke, autoActivationCharacters); + + // Listen for popup open/close events to be able to handle focus events correctly + contentProposalAdapter.addContentProposalListener(new IContentProposalListener2() { + + public void proposalPopupClosed(ContentProposalAdapter adapter) { + popupOpen = false; + } + + public void proposalPopupOpened(ContentProposalAdapter adapter) { + popupOpen = true; + } + }); + } + + /** + * Return the {@link ContentProposalAdapter} of this cell editor. + * + * @return the {@link ContentProposalAdapter} + */ + public ContentProposalAdapter getContentProposalAdapter() { + return contentProposalAdapter; + } + + + /** + * Checks to see if the "deletable" state (can delete/ + * nothing to delete) has changed and if so fire an + * enablement changed notification. + */ + private void checkDeleteable() { + boolean oldIsDeleteable = isDeleteable; + isDeleteable = isDeleteEnabled(); + if (oldIsDeleteable != isDeleteable) { + fireEnablementChanged(DELETE); + } + } + + /** + * Checks to see if the "selectable" state (can select) + * has changed and if so fire an enablement changed notification. + */ + private void checkSelectable() { + boolean oldIsSelectable = isSelectable; + isSelectable = isSelectAllEnabled(); + if (oldIsSelectable != isSelectable) { + fireEnablementChanged(SELECT_ALL); + } + } + + /** + * Checks to see if the selection state (selection / + * no selection) has changed and if so fire an + * enablement changed notification. + */ + private void checkSelection() { + boolean oldIsSelection = isSelection; + isSelection = text.getSelectionCount() > 0; + if (oldIsSelection != isSelection) { + fireEnablementChanged(COPY); + fireEnablementChanged(CUT); + } + } + + @Override + protected Control createControl(Composite parent) { + text = new StyledText(parent, getStyle()); + text.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + handleDefaultSelection(e); + } + }); + text.addKeyListener(new KeyAdapter() { + // hook key pressed - see PR 14201 + @Override + public void keyPressed(KeyEvent e) { + keyReleaseOccured(e); + + // as a result of processing the above call, clients may have + // disposed this cell editor + if ((getControl() == null) || getControl().isDisposed()) { + return; + } + checkSelection(); // see explanation below + checkDeleteable(); + checkSelectable(); + } + }); + text.addTraverseListener(new TraverseListener() { + @Override + public void keyTraversed(TraverseEvent e) { + if (e.detail == SWT.TRAVERSE_ESCAPE + || e.detail == SWT.TRAVERSE_RETURN) { + e.doit = false; + } + } + }); + // We really want a selection listener but it is not supported so we + // use a key listener and a mouse listener to know when selection changes + // may have occurred + text.addMouseListener(new MouseAdapter() { + @Override + public void mouseUp(MouseEvent e) { + checkSelection(); + checkDeleteable(); + checkSelectable(); + } + }); + text.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + StyledTextCellEditor.this.focusLost(); + } + }); + text.setFont(parent.getFont()); + text.setBackground(parent.getBackground()); + text.setText("");//$NON-NLS-1$ + text.addModifyListener(getModifyListener()); + return text; + } + + /** + * The TextCellEditor implementation of + * this CellEditor framework method returns + * the text string. + * + * @return the text string + */ + @Override + protected Object doGetValue() { + return text.getText(); + } + + @Override + protected void doSetFocus() { + if (text != null) { + text.selectAll(); + text.setFocus(); + checkSelection(); + checkDeleteable(); + checkSelectable(); + } + } + + /** + * The TextCellEditor implementation of + * this CellEditor framework method accepts + * a text string (type String). + * + * @param value a text string (type String) + */ + @Override + protected void doSetValue(Object value) { + Assert.isTrue(text != null && (value instanceof String)); + text.removeModifyListener(getModifyListener()); + text.setText((String) value); + text.setStyleRanges(updateStyleRanges(text.getText())); + text.addModifyListener(getModifyListener()); + } + + /** + * Processes a modify event that occurred in this text cell editor. + * This framework method performs validation and sets the error message + * accordingly, and then reports a change via fireEditorValueChanged. + * Subclasses should call this method at appropriate times. Subclasses + * may extend or reimplement. + * + * @param e the SWT modify event + */ + protected void editOccured(ModifyEvent e) { + String value = text.getText(); + if (value == null) { + value = "";//$NON-NLS-1$ + } + Object typedValue = value; + boolean oldValidState = isValueValid(); + boolean newValidState = isCorrect(typedValue); + if (!newValidState) { + // try to insert the current value into the error message. + setErrorMessage(MessageFormat.format(getErrorMessage(), + new Object[] { value })); + } + valueChanged(oldValidState, newValidState); + } + + /** + * Since a text editor field is scrollable we don't + * set a minimumSize. + */ + @Override + public LayoutData getLayoutData() { + LayoutData data = new LayoutData(); + data.minimumWidth= 0; + return data; + } + + /** + * Return the modify listener. + */ + private ModifyListener getModifyListener() { + if (modifyListener == null) { + modifyListener = new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + text.setStyleRanges(updateStyleRanges(text.getText())); + editOccured(e); + } + }; + } + return modifyListener; + } + + /** + * Handles a default selection event from the text control by applying the editor + * value and deactivating this cell editor. + * + * @param event the selection event + * + * @since 3.0 + */ + protected void handleDefaultSelection(SelectionEvent event) { + // same with enter-key handling code in keyReleaseOccured(e); + fireApplyEditorValue(); + deactivate(); + } + + /** + * The TextCellEditor implementation of this + * CellEditor method returns true if + * the current selection is not empty. + */ + @Override + public boolean isCopyEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return text.getSelectionCount() > 0; + } + + /** + * The TextCellEditor implementation of this + * CellEditor method returns true if + * the current selection is not empty. + */ + @Override + public boolean isCutEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return text.getSelectionCount() > 0; + } + + /** + * The TextCellEditor implementation of this + * CellEditor method returns true + * if there is a selection or if the caret is not positioned + * at the end of the text. + */ + @Override + public boolean isDeleteEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return text.getSelectionCount() > 0 + || text.getCaretOffset() < text.getCharCount(); + } + + /** + * The TextCellEditor implementation of this + * CellEditor method always returns true. + */ + @Override + public boolean isPasteEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return true; + } + + /** + * Check if save all is enabled + * @return true if it is + */ + public boolean isSaveAllEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return true; + } + + /** + * Returns true if this cell editor is + * able to perform the select all action. + *

+ * This default implementation always returns + * false. + *

+ *

+ * Subclasses may override + *

+ * @return true if select all is possible, + * false otherwise + */ + @Override + public boolean isSelectAllEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return text.getCharCount() > 0; + } + + /** + * Processes a key release event that occurred in this cell editor. + *

+ * The TextCellEditor implementation of this framework method + * ignores when the RETURN key is pressed since this is handled in + * handleDefaultSelection. + * An exception is made for Ctrl+Enter for multi-line texts, since + * a default selection event is not sent in this case. + *

+ * + * @param keyEvent the key event + */ + @Override + protected void keyReleaseOccured(KeyEvent keyEvent) { + if (keyEvent.character == '\r') { // Return key + // Enter is handled in handleDefaultSelection. + // Do not apply the editor value in response to an Enter key event + // since this can be received from the IME when the intent is -not- + // to apply the value. + // See bug 39074 [CellEditors] [DBCS] canna input mode fires bogus event from Text Control + // + // An exception is made for Ctrl+Enter for multi-line texts, since + // a default selection event is not sent in this case. + if (text != null && !text.isDisposed() + && (text.getStyle() & SWT.MULTI) != 0) { + if ((keyEvent.stateMask & SWT.CTRL) != 0) { + super.keyReleaseOccured(keyEvent); + } + } + if (popupOpen) { + return; + } + } + super.keyReleaseOccured(keyEvent); + } + + /** + * The TextCellEditor implementation of this + * CellEditor method copies the + * current selection to the clipboard. + */ + @Override + public void performCopy() { + text.copy(); + } + + /** + * The TextCellEditor implementation of this + * CellEditor method cuts the + * current selection to the clipboard. + */ + @Override + public void performCut() { + text.cut(); + checkSelection(); + checkDeleteable(); + checkSelectable(); + } + + /** + * The TextCellEditor implementation of this + * CellEditor method deletes the + * current selection or, if there is no selection, + * the character next character from the current position. + */ + @Override + public void performDelete() { + if (text.getSelectionCount() > 0) { + // remove the contents of the current selection + text.insert(""); //$NON-NLS-1$ + } else { + // remove the next character + int pos = text.getCaretOffset(); + if (pos < text.getCharCount()) { + text.setSelection(pos, pos + 1); + text.insert(""); //$NON-NLS-1$ + } + } + checkSelection(); + checkDeleteable(); + checkSelectable(); + } + + /** + * The TextCellEditor implementation of this + * CellEditor method pastes the + * the clipboard contents over the current selection. + */ + @Override + public void performPaste() { + text.paste(); + checkSelection(); + checkDeleteable(); + checkSelectable(); + } + + /** + * The TextCellEditor implementation of this + * CellEditor method selects all of the + * current text. + */ + @Override + public void performSelectAll() { + text.selectAll(); + checkSelection(); + checkDeleteable(); + } + + /** + * This implementation of + * {@link CellEditor#dependsOnExternalFocusListener()} returns false if the + * current instance's class is TextCellEditor, and true otherwise. + * Subclasses that hook their own focus listener should override this method + * and return false. See also bug 58777. + * + * @since 3.4 + */ + @Override + protected boolean dependsOnExternalFocusListener() { + return false; + } + + @Override + protected void focusLost() { + if (!popupOpen) { + // Focus lost deactivates the cell editor. + // This must not happen if focus lost was caused by activating + // the completion proposal popup. + super.focusLost(); + } + } + + abstract protected StyleRange[] updateStyleRanges(String text); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/StyledTextContentAdapter.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/StyledTextContentAdapter.java new file mode 100644 index 000000000..0e771e293 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/StyledTextContentAdapter.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.jface.fieldassist.IControlContentAdapter; +import org.eclipse.jface.fieldassist.IControlContentAdapter2; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Control; + +/** + * Content Adapter for StyledText widget + * + * @author Alex Boyko + * + */ +public class StyledTextContentAdapter implements IControlContentAdapter, IControlContentAdapter2 { + + @Override + public String getControlContents(Control control) { + StyledText styledText = (StyledText) control; + return styledText.getText(); + } + + @Override + public void setControlContents(Control control, String text, + int cursorPosition) { + StyledText styledText = (StyledText) control; + styledText.setText(text); + styledText.setSelection(cursorPosition, cursorPosition); + } + + @Override + public void insertControlContents(Control control, String text, + int cursorPosition) { + StyledText styledText = (StyledText) control; + Point selection = styledText.getSelection(); + styledText.insert(text); + // Insert will leave the cursor at the end of the inserted text. If this + // is not what we wanted, reset the selection. + if (cursorPosition < text.length()) { + styledText.setSelection(selection.x + cursorPosition, + selection.x + cursorPosition); + } + } + + @Override + public int getCursorPosition(Control control) { + StyledText styledText = (StyledText) control; + return styledText.getCaretOffset(); + } + + @Override + public Rectangle getInsertionBounds(Control control) { + StyledText styledText = (StyledText) control; + return styledText.getCaret().getBounds(); + } + + @Override + public void setCursorPosition(Control control, int position) { + StyledText styledText = (StyledText) control; + styledText.setSelection(new Point(position, position)); + } + + @Override + public Point getSelection(Control control) { + StyledText styledText = (StyledText) control; + return styledText.getSelection(); + } + + @Override + public void setSelection(Control control, Point range) { + StyledText styledText = (StyledText) control; + styledText.setSelection(range); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/StyledTextEditor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/StyledTextEditor.java new file mode 100644 index 000000000..4c9c46425 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/StyledTextEditor.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.jface.bindings.keys.KeyStroke; +import org.eclipse.jface.fieldassist.ContentProposalAdapter; +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.eclipse.swt.widgets.Composite; + +/** + * Styled Text editor control. It's based on the corresponding cell editor. The + * difference is that tries to give away focus (yield to the active shell) when + * editing is completed or cancelled + * + * @author Alex Boyko + * + */ +public abstract class StyledTextEditor extends StyledTextCellEditor { + + public StyledTextEditor(Composite parent) { + super(parent); + } + + public StyledTextEditor(Composite parent, IContentProposalProvider contentProposalProvider, + KeyStroke keyStroke, char[] autoActivationCharacters) { + super(parent, contentProposalProvider, keyStroke, autoActivationCharacters); + setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE); + } + + @Override + public void deactivate() { + if (text.getDisplay() != null && text.isFocusControl() && text.getDisplay().getActiveShell() != null) { + text.getDisplay().getActiveShell().forceFocus(); + } + } + + @Override + protected void fireCancelEditor() { + super.fireCancelEditor(); + deactivate(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagContentProposalProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagContentProposalProvider.java new file mode 100644 index 000000000..87dad431a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagContentProposalProvider.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import org.eclipse.jface.fieldassist.IContentProposal; +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.TagUtils; +import org.springframework.ide.eclipse.editor.support.util.FuzzyMatcher; + +/** + * Provider for tags content proposals + * + * @author Alex Boyko + * + */ +public class TagContentProposalProvider implements IContentProposalProvider { + + private BootDashViewModel model; + + public TagContentProposalProvider(BootDashViewModel model) { + this.model = model; + } + + private Set getAllTags() { + HashSet tags = new HashSet(); + for (BootDashModel sectionModel : model.getSectionModels().getValue()) { + for (BootDashElement element : sectionModel.getElements().getValue()) { + tags.addAll(element.getTags()); + } + } + return tags; + } + + @Override + public IContentProposal[] getProposals(String contents, int position) { + Set allTags = getAllTags(); + + int startPosition = getCurrentTagStartPosition(contents, position); + int endPosition = getCurrentTagEndPosition(contents, position); + String pattern = contents.substring(startPosition, position); + String tagUnderCursor = contents.substring(startPosition, endPosition); + HashSet currentTags = new HashSet(Arrays.asList(TagUtils.parseTags(contents))); + if (pattern.equals(tagUnderCursor)) { + currentTags.remove(tagUnderCursor); + } + allTags.removeAll(currentTags); + + List proposals = new ArrayList(allTags.size()); + for (String tag : allTags) { + if (FuzzyMatcher.matchScore(pattern, tag) != 0) { + proposals.add(tagProposal(tag, applyProposal(contents, tag, startPosition, position), startPosition + tag.length())); + } + } + return proposals.toArray(new IContentProposal[proposals.size()]); + } + + private static int getCurrentTagStartPosition(String content, int position) { + int i = position - 1; + for (; i >= 0 && content.charAt(i) != TagUtils.SEPARATOR_SYMBOL; i--); + i++; + for (; i < content.length() && Pattern.matches("\\s", "" + content.charAt(i)); i++); + return i; + } + + private static int getCurrentTagEndPosition(String content, int position) { + int i = position; + for (; i < content.length() && content.charAt(i) != TagUtils.SEPARATOR_SYMBOL; i++); + for (; i > position && Pattern.matches("\\s", "" + content.charAt(i - 1)); i--); + return i; + } + + private static String applyProposal(String content, String proposal, int start, int end) { + StringBuilder sb = new StringBuilder(start + proposal.length() + content.length() - end); + sb.append(content.substring(0, start)); + sb.append(proposal); + sb.append(content.substring(end)); + return sb.toString(); + } + + private IContentProposal tagProposal(final String label, final String newContent, final int cursorPosition) { + return new IContentProposal() { + + @Override + public String getLabel() { + return label; + } + + @Override + public String getDescription() { + return null; + } + + @Override + public int getCursorPosition() { + return cursorPosition; + } + + @Override + public String getContent() { + return newContent; + } + + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagEditingSupport.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagEditingSupport.java new file mode 100644 index 000000000..2ca49c49a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagEditingSupport.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import java.util.Arrays; +import java.util.LinkedHashSet; + +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.EditingSupport; +import org.eclipse.jface.viewers.TableViewer; +import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springframework.ide.eclipse.boot.dash.model.TagUtils; +import org.springframework.ide.eclipse.boot.dash.model.Taggable; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +/** + * Support for editing tags with the text cell editor + * + * @author Alex Boyko + * + */ +public class TagEditingSupport extends EditingSupport { + + private StyledTextCellEditor editor; + + public TagEditingSupport(TableViewer viewer, LiveExpression selection, Stylers stylers) { + super(viewer); + this.editor = new TagsCellEditor(viewer.getTable(), stylers); + } + + public TagEditingSupport(TableViewer viewer, LiveExpression selection, BootDashViewModel model, Stylers stylers) { + super(viewer); + IContentProposalProvider proposalProvider = new TagContentProposalProvider(model); + this.editor = new TagsCellEditor(viewer.getTable(), stylers, proposalProvider, UIUtils.CTRL_SPACE, + UIUtils.TAG_CA_AUTO_ACTIVATION_CHARS); + } + + @Override + protected CellEditor getCellEditor(Object element) { + return editor; + } + + @Override + protected boolean canEdit(Object element) { + return element instanceof Taggable; + } + + @Override + protected Object getValue(Object element) { + if (element instanceof Taggable) { + return TagUtils.toString(((Taggable)element).getTags()); + } else { + return null; + } + } + + @Override + protected void setValue(Object element, Object value) { + if (element instanceof Taggable && value instanceof String) { + String str = (String) value; + Taggable taggable = (Taggable) element; + if (str.isEmpty()) { + taggable.setTags(null); + } else { + taggable.setTags(new LinkedHashSet<>(Arrays.asList(TagUtils.parseTags(str)))); + } + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagSearchSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagSearchSection.java new file mode 100644 index 000000000..c6645ec2c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagSearchSection.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.jface.fieldassist.ContentProposalAdapter; +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.eclipse.jface.fieldassist.TextContentAdapter; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; +import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +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.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.PageSection; + +/** + * A text box with the look and feel of a search box. The contents + * of the searchbox text is mirrored into a LiveVariable 'model'. + * + * @author Alex Boyko + * @author Kris De Volder + */ +public class TagSearchSection extends PageSection implements Disposable { + + private Text tagsSearchBox; + private BootDashViewModel viewModel; + private LiveVariable model; + private ValueListener modelListener; + + public TagSearchSection(IPageWithSections owner, LiveVariable model, BootDashViewModel viewModel) { + super(owner); + this.model = model; + this.viewModel = viewModel; + } + + @Override + public LiveExpression getValidator() { + return OK_VALIDATOR; + } + + @Override + public void createContents(Composite page) { + tagsSearchBox = new Text(page, SWT.SINGLE | SWT.BORDER | SWT.SEARCH | SWT.ICON_CANCEL); + tagsSearchBox.setMessage("Type tags, projects, or working set names to match (incl. * and ? wildcards)"); + tagsSearchBox.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create()); + tagsSearchBox.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + model.setValue(tagsSearchBox.getText()); + } + }); + this.model.addListener(modelListener = new ValueListener() { + public void gotValue(LiveExpression exp, String newText) { + String oldText = tagsSearchBox.getText(); + if (!oldText.equals(newText)) { //Avoid cursor bug on macs. + tagsSearchBox.setText(newText); + } + } + }); + + IContentProposalProvider proposalProvider = new TagContentProposalProvider(viewModel); + ContentProposalAdapter caAdapter = new ContentProposalAdapter(tagsSearchBox, new TextContentAdapter(), proposalProvider, UIUtils.CTRL_SPACE, null); + caAdapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE); + + } + + @Override + public void dispose() { + if (modelListener!=null) { + this.model.removeListener(modelListener); + modelListener = null; + } + tagsSearchBox.dispose(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagsCellEditor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagsCellEditor.java new file mode 100644 index 000000000..417f28551 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagsCellEditor.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.jface.bindings.keys.KeyStroke; +import org.eclipse.jface.fieldassist.ContentProposalAdapter; +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.widgets.Composite; +import org.springframework.ide.eclipse.boot.dash.model.TagUtils; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +/** + * Cell editor for tags + * + * @author Alex Boyko + * + */ +public class TagsCellEditor extends StyledTextCellEditor { + + private Stylers stylers; + + public TagsCellEditor(Composite parent, Stylers stylers) { + super(parent); + this.stylers = stylers; + } + + public TagsCellEditor(Composite parent, Stylers stylers, IContentProposalProvider contentProposalProvider, + KeyStroke keyStroke, char[] autoActivationCharacters) { + super(parent, contentProposalProvider, keyStroke, autoActivationCharacters); + setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE); + this.stylers = stylers; + } + + @Override + protected StyleRange[] updateStyleRanges(String text) { + StyledString styled = TagUtils.applyTagStyles(text, stylers.tag()); + return styled.getStyleRanges(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagsEditor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagsEditor.java new file mode 100644 index 000000000..dff617a34 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/TagsEditor.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.jface.bindings.keys.KeyStroke; +import org.eclipse.jface.fieldassist.ContentProposalAdapter; +import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.widgets.Composite; +import org.springframework.ide.eclipse.boot.dash.model.TagUtils; +import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; + +/** + * Tags editor based on StyledText cell editor + * + * @author Alex Boyko + * + */ +public class TagsEditor extends StyledTextEditor { + + private Stylers stylers; + + public TagsEditor(Composite parent, Stylers stylers) { + super(parent); + this.stylers = stylers; + } + + public TagsEditor(Composite parent, Stylers stylers, IContentProposalProvider contentProposalProvider, + KeyStroke keyStroke, char[] autoActivationCharacters) { + super(parent, contentProposalProvider, keyStroke, autoActivationCharacters); + setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE); + this.stylers = stylers; + } + + @Override + protected StyleRange[] updateStyleRanges(String text) { + StyledString styled = TagUtils.applyTagStyles(text, stylers.tag()); + return styled.getStyleRanges(); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/UIUtils.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/UIUtils.java new file mode 100644 index 000000000..b5f7f5ff0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/views/sections/UIUtils.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2015 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.views.sections; + +import org.eclipse.jface.bindings.keys.KeyStroke; +import org.eclipse.swt.SWT; + +/** + * SWT/JFace dependent utility methods and constants + * + * @author Alex Boyko + * + */ +public class UIUtils { + + public static final char[] PATH_CA_AUTO_ACTIVATION_CHARS = "/.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); + + public static final char[] TAG_CA_AUTO_ACTIVATION_CHARS = "/,-.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); + + public static final KeyStroke CTRL_SPACE = KeyStroke.getInstance(SWT.CTRL, SWT.SPACE); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.classpath new file mode 100644 index 000000000..3e5654f17 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.project new file mode 100644 index 000000000..ffac8dea4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.project @@ -0,0 +1,39 @@ + + + org.springframework.ide.eclipse.boot.launch.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + + + 1443121011770 + + 14 + + org.eclipse.ui.ide.multiFilter + 1.0-projectRelativePath-matches-false-false-target + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..0c68a61dc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.settings/org.eclipse.jdt.ui.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..c743e1c07 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,59 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=false +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_type_arguments=false +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=false +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/META-INF/MANIFEST.MF b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/META-INF/MANIFEST.MF new file mode 100644 index 000000000..3856c1891 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/META-INF/MANIFEST.MF @@ -0,0 +1,26 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Boot Launch Tests +Bundle-SymbolicName: org.springframework.ide.eclipse.boot.launch.test +Bundle-Version: 4.8.1.qualifier +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.junit, + org.springframework.ide.eclipse.boot.launch, + org.springsource.ide.eclipse.commons.tests.util, + org.eclipse.core.resources, + org.springsource.ide.eclipse.commons.livexp, + org.eclipse.debug.core, + org.eclipse.jdt.launching, + org.eclipse.jdt.core, + org.eclipse.jdt.debug.ui, + org.springframework.ide.eclipse.boot, + org.springsource.ide.eclipse.commons.frameworks.core, + org.springframework.ide.eclipse.boot.test, + org.springsource.ide.eclipse.commons.frameworks.test.util, + org.apache.commons.io, + org.apache.commons.lang3, + org.springframework.ide.eclipse.buildship30 +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Vendor: GoPivotal Inc. diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/about.html b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/about.html new file mode 100644 index 000000000..55dfadc68 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/about.html @@ -0,0 +1,248 @@ + + + + +About Spring IDE + + + +

About Spring IDE

+

March 26, 2007

+

Abstract

+

Spring IDE is a set of plugins which provide a user interface for +The Spring Framework's bean +factory XML configuration files including support for Spring 2.0 +namespaces, AOP configuration and Spring Web Flow.

+

License

+ +

The Spring IDE Project makes available all content in this +plug-in ("Content"). Unless otherwise indicated below, the +Content is provided to you under the terms and conditions of the Eclipse +Public License Version 1.0 ("EPL"). A copy of the EPL is +available at https://www.eclipse.org/legal/epl-v10.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the Spring IDE +Project, the Content is being redistributed by another party +("Redistributor") and different terms and conditions may apply +to your use of any object code in the Content. Check the Redistributor's +license that was provided with the Content. If no such license exists, +contact the Redistributor. Unless otherwise indicated below, the terms +and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at https://www.eclipse.org.

+ +

Third Party Content

+

The Content includes items that have been sourced from third +parties as set out below. If you did not receive this Content directly +from the Spring IDE Project, the following is provided for informational +purposes only, and you should look to the Redistributor's license for +terms and conditions of use.

+

The Spring Framework v3.0.5 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Web Flow v2.0.7.A +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Web Services v1.5.8 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Security v3.0.2 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Dynamic Modules for OSGi(TM) Runtimes v2.0.0.M2 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Batch v2.1.0 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Commons Codec v1.3.0 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

Commons Collections v3.2.0 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

Commons Logging v1.1.1 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

Apache MyFaces v1.2.2 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

AspectJ Weaver v1.6.10 +

This product includes software developed by The Eclipse Software +Foundation (https://www.eclipse.org/).

+ +

Antlr v3.0.1 +

Spring IDE includes a binary version of Antlr v3.0.1 (https://www.antlr.org/). +The source code for Antlr is available from the Antlr download site at +https://www.antlr.org/download.html. + +

The Antlr license is available at https://www.antlr.org/license.html. +The license is also reproduced here:

+ +
Copyright (c) 2003-2007, Terence Parr
+All rights reserved.
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions 
+and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions 
+and the following disclaimer in the documentation and/or other materials provided with the distribution.
+Neither the name of the author nor the names of its contributors may be used to endorse or promote 
+products derived from this software without specific prior written permission.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+

+ + +

ASM v2.2.3 +

Spring IDE includes a binary version of ASM v2.2.3 (https://asm.objectweb.org/). +The source code for ASM is available from the ObjectWeb download site at +https://asm.objectweb.org/download/. + +

The ASM license is available at https://asm.objectweb.org/license.html. +The license is also reproduced here:

+ +
Copyright (c) 2000-2005 INRIA, France Telecom
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holders nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
+

+ + +

backport-util-concurrent 3.1.0 +

Spring IDE includes a binary version of backport-util-concurrent v3.1.0 (https://dcl.mathcs.emory.edu/util/backport-util-concurrent/). +The source code for backport-util-concurrent is available from the download site at +https://dcl.mathcs.emory.edu/util/backport-util-concurrent/dist/. + + +

The backport-util-concurrent license is available at https://creativecommons.org/licenses/publicdomain. +The license is also reproduced here:

+ +
Copyright-Only Dedication (based on United States law) or Public Domain Certification
+
+The person or persons who have associated work with this document (the 
+"Dedicator" or "Certifier") hereby either (a) certifies that, to the 
+best of his knowledge, the work of authorship identified is in the public 
+domain of the country from which the work is published, or (b) hereby 
+dedicates whatever copyright the dedicators holds in the work of authorship 
+identified below (the "Work") to the public domain. A certifier, moreover, 
+dedicates any copyright interest he may have in the associated work, and 
+for these purposes, is described as a "dedicator" below.
+
+A certifier has taken reasonable steps to verify the copyright status of 
+this work. Certifier recognizes that his good faith efforts may not shield 
+him from liability if in fact the work certified is not in the public domain.
+
+Dedicator makes this dedication for the benefit of the public at large and 
+to the detriment of the Dedicator's heirs and successors. Dedicator intends 
+this dedication to be an overt act of relinquishment in perpetuity of all 
+present and future rights under copyright law, whether vested or contingent, 
+in the Work. Dedicator understands that such relinquishment of all rights 
+includes the relinquishment of all rights to enforce (by lawsuit or otherwise) 
+those copyrights in the Work.
+
+Dedicator recognizes that, once placed in the public domain, the Work may be 
+freely reproduced, distributed, transmitted, used, modified, built upon, or 
+otherwise exploited by anyone for any purpose, commercial or non-commercial, 
+and in any way, including by methods that have not yet been invented or 
+conceived.
+

+ +

AOP Alliance +

LICENCE: all the source code provided by AOP Alliance is Public Domain. +

+ +

javax.el v1.0.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.jms v1.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.servlet v2.5.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.servlet.jsp v2.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.servlet.jsp.jstl v1.2.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.activation v1.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.annotation v1.0.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.mail v1.4.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.xml.stream v1.0.1 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.xml.ws v2.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

+ + +
Copyright (c) 2005, 2011 Spring IDE Developers
+ + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/build.properties b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/build.properties new file mode 100644 index 000000000..2cd837884 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/build.properties @@ -0,0 +1,6 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + about.html,\ + workspace/ diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/pom.xml new file mode 100644 index 000000000..ef7206eeb --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + org.springframework.boot.ide + org.springframework.boot.ide + 4.8.1-SNAPSHOT + ../../eclipse-distribution/pom.xml + + + org.springframework.ide.eclipse + org.springframework.ide.eclipse.boot.launch.test + eclipse-test-plugin + + + + + org.eclipse.tycho + tycho-surefire-plugin + ${tycho-version} + + true + 7200 + org.springframework.ide.eclipse.boot.launch.test + org.springframework.ide.eclipse.boot.launch.test.AllBootLaunchTests + + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + + + eclipse-plugin + org.eclipse.equinox.event + 0.0.0 + + + eclipse-plugin + org.apache.felix.scr + 0.0.0 + + + eclipse-feature + org.eclipse.m2e.feature + 0.0.0 + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/AllBootLaunchTests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/AllBootLaunchTests.java new file mode 100644 index 000000000..81b414a15 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/AllBootLaunchTests.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2015 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch.test; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +@RunWith(Suite.class) +@SuiteClasses({ + BootLaunchUIModelTest.class, + ProfileHistoryTest.class, + BootLaunchConfigurationDelegateTest.class, + BootLaunchShortcutTest.class, + BootGroovyScriptLaunchConfigurationDelegateTest.class +}) +public class AllBootLaunchTests { + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootGroovyScriptLaunchConfigurationDelegateTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootGroovyScriptLaunchConfigurationDelegateTest.java new file mode 100644 index 000000000..118f4865e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootGroovyScriptLaunchConfigurationDelegateTest.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2013, 2017 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch.test; + +import static org.springframework.ide.eclipse.boot.launch.cli.BootGroovyScriptLaunchConfigurationDelegate.setScript; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.springframework.ide.eclipse.boot.core.cli.BootInstallManager; +import org.springframework.ide.eclipse.boot.launch.cli.BootGroovyScriptLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.test.util.LaunchResult; +import org.springframework.ide.eclipse.boot.test.util.LaunchUtil; + +/** + * @author Kris De Volder + */ +public class BootGroovyScriptLaunchConfigurationDelegateTest extends BootLaunchTestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + BootInstallManager.getInstance().getDownloader().allowUIThread(true); + } + + private static final String PROJECT_NAME = BootGroovyScriptLaunchConfigurationDelegateTest.class.getSimpleName(); + + private ILaunchConfigurationWorkingCopy createWorkingCopy() throws CoreException { + return createWorkingCopy(BootGroovyScriptLaunchConfigurationDelegate.ID); + } + + public void testLaunchHelloWorld() throws Exception { + IProject p = createGeneralProject(PROJECT_NAME); + createFile(p, "app.groovy", + "@Component\n" + + "class App implements CommandLineRunner {\n" + + " void run(String... args) {\n" + + " sleep(1000)\n" + + " println \"Hello, world!\"\n" + + " }\n" + + "}" + ); + + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + setScript(wc, p.getFile("app.groovy")); + + + LaunchResult result = LaunchUtil.synchLaunch(wc); + System.out.println(result); //Great help in debugging this :-) + assertContains("Hello, world", result.out); + assertOk(result); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootLaunchConfigurationDelegateTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootLaunchConfigurationDelegateTest.java new file mode 100644 index 000000000..3a4ebb29e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootLaunchConfigurationDelegateTest.java @@ -0,0 +1,647 @@ +/******************************************************************************* + * Copyright (c) 2015, 2018 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch.test; + +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.withImportStrategy; + +import java.io.File; +import java.net.URL; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.function.Predicate; + +import javax.xml.transform.stream.StreamResult; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.internal.core.IInternalDebugCoreConstants; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jdt.launching.JavaLaunchDelegate; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.core.BootPreferences; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.livebean.JmxBeanSupport; +import org.springframework.ide.eclipse.boot.launch.process.BootProcessFactory; +import org.springframework.ide.eclipse.boot.test.util.LaunchResult; +import org.springframework.ide.eclipse.boot.test.util.LaunchUtil; +import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; +import org.springsource.ide.eclipse.commons.frameworks.test.util.Timewatch; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +/** + * @author Kris De Volder + */ +@SuppressWarnings("restriction") +public class BootLaunchConfigurationDelegateTest extends BootLaunchTestCase { + + private static final String TEST_MAIN_CLASS = "demo.DumpInfoApplication"; + private static final String TEST_PROJECT = "dump-info"; + + @Override + protected void setUp() throws Exception { + super.setUp(); + //The following disables some nasty popups that can cause test to hang rather than fail. + //when project has errors upon launching it. + InstanceScope.INSTANCE.getNode(DebugPlugin.getUniqueIdentifier()) + .putBoolean(IInternalDebugCoreConstants.PREF_ENABLE_STATUS_HANDLERS, false); + BootActivator.getDefault().getPreferenceStore().setToDefault(BootPreferences.PREF_BOOT_FAST_STARTUP_JVM_ARGS); + BootActivator.getDefault().getPreferenceStore().setToDefault(BootPreferences.PREF_BOOT_FAST_STARTUP_DEFAULT); + } + + @SafeVarargs + private static final Properties buildPropewrties(Pair... pvs) { + Properties props = new Properties(); + for (Pair pv : pvs) { + props.setProperty(pv.getKey(), pv.getValue()); + } + return props; + } + + public void testGetSetProperties() throws Exception { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + assertEquals("", BootLaunchConfigurationDelegate.getRawApplicationProperties(wc)); + + BootLaunchConfigurationDelegate.setRawApplicationProperties(wc, null); //accepts null in lieu of empty list, + assertEquals("", BootLaunchConfigurationDelegate.getRawApplicationProperties(wc)); + + //store one single property + doGetAndSetProps(wc, + "foo=Hello", + buildPropewrties(Pair.of("foo", "Hello")) + ); + + //store empty property list + doGetAndSetProps(wc, + "", + buildPropewrties() + ); + + //store a few properties + doGetAndSetProps(wc, + "foo.bar: snuffer.nazz\n" + + "#neala: nolo\n" + + "#Hohoh: Santa Claus", + buildPropewrties(Pair.of("foo.bar", "snuffer.nazz")) + ); + + //store properties with identical keys + doGetAndSetProps(wc, + "foo=snuffer.nazz\n" + + "#foo=nolo\n" + + "#bar=Santa Claus\n" + + "#bar=Santkkk ", + buildPropewrties(Pair.of("foo", "snuffer.nazz")) + ); + } + + private ILaunchConfigurationWorkingCopy createWorkingCopy() throws CoreException { + return createWorkingCopy(BootLaunchConfigurationDelegate.TYPE_ID); + } + + private void doGetAndSetProps(ILaunchConfigurationWorkingCopy wc, String props, Properties expected) { + BootLaunchConfigurationDelegate.setRawApplicationProperties(wc, props); + Properties retrieved = BootLaunchConfigurationDelegate.getApplicationProperties(wc); + assertEquals(expected, retrieved); + } + + public void testSetGetProject() throws Exception { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + assertEquals(null, BootLaunchConfigurationDelegate.getProject(wc)); + IProject project = getProject("foo"); + BootLaunchConfigurationDelegate.setProject(wc, project); + assertEquals(project, BootLaunchConfigurationDelegate.getProject(wc)); + } + + public void testSetGetProfile() throws Exception { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + assertEquals("", BootLaunchConfigurationDelegate.getProfile(wc)); + + BootLaunchConfigurationDelegate.setProfile(wc, "deployment"); + assertEquals("deployment", BootLaunchConfigurationDelegate.getProfile(wc)); + + BootLaunchConfigurationDelegate.setProfile(wc, null); + assertEquals("", BootLaunchConfigurationDelegate.getProfile(wc)); + } + + public void testSetGetAnsiConsoleOutput() throws Exception { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + boolean ideSupportsAnsiConsoleOutput = BootLaunchConfigurationDelegate.supportsAnsiConsoleOutput(); + assertEquals(ideSupportsAnsiConsoleOutput, BootLaunchConfigurationDelegate.getEnableAnsiConsoleOutput(wc)); + + BootLaunchConfigurationDelegate.setEnableAnsiConsoleOutput(wc, true); + assertEquals(true, BootLaunchConfigurationDelegate.getEnableAnsiConsoleOutput(wc)); + + BootLaunchConfigurationDelegate.setEnableAnsiConsoleOutput(wc, false); + assertEquals(false, BootLaunchConfigurationDelegate.getEnableAnsiConsoleOutput(wc)); + } + + public void testSetGetFastStartup() throws Exception { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + assertEquals( + BootActivator.getDefault().getPreferenceStore() + .getBoolean(BootPreferences.PREF_BOOT_FAST_STARTUP_DEFAULT), + BootLaunchConfigurationDelegate.getFastStartup(wc)); + + BootLaunchConfigurationDelegate.setFastStartup(wc, true); + assertEquals(true, BootLaunchConfigurationDelegate.getFastStartup(wc)); + + BootLaunchConfigurationDelegate.setFastStartup(wc, false); + assertEquals(false, BootLaunchConfigurationDelegate.getFastStartup(wc)); + } + + public void testClearProperties() throws Exception { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + BootLaunchConfigurationDelegate.setRawApplicationProperties(wc, + "some=thing\n" + + "#some.other=thing" + ); + assertFalse(BootLaunchConfigurationDelegate.getApplicationProperties(wc).isEmpty()); + BootLaunchConfigurationDelegate.clearProperties(wc); + assertTrue(BootLaunchConfigurationDelegate.getApplicationProperties(wc).isEmpty()); + } + + public void testGetSetDebug() throws Exception { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + boolean deflt = BootLaunchConfigurationDelegate.DEFAULT_ENABLE_DEBUG_OUTPUT; + boolean other = !deflt; + assertEquals(deflt, + BootLaunchConfigurationDelegate.getEnableDebugOutput(wc)); + + BootLaunchConfigurationDelegate.setEnableDebugOutput(wc, other); + assertEquals(other, BootLaunchConfigurationDelegate.getEnableDebugOutput(wc)); + + BootLaunchConfigurationDelegate.setEnableDebugOutput(wc, deflt); + assertEquals(deflt, BootLaunchConfigurationDelegate.getEnableDebugOutput(wc)); + } + + public void testGetSetLiveBean() throws Exception { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + boolean deflt = BootLaunchConfigurationDelegate.DEFAULT_ENABLE_LIVE_BEAN_SUPPORT(); + boolean other = !deflt; + + assertEquals(deflt, BootLaunchConfigurationDelegate.getEnableLiveBeanSupport(wc)); + + BootLaunchConfigurationDelegate.setEnableLiveBeanSupport(wc, other); + assertEquals(other, BootLaunchConfigurationDelegate.getEnableLiveBeanSupport(wc)); + + BootLaunchConfigurationDelegate.setEnableLiveBeanSupport(wc, deflt); + assertEquals(deflt, BootLaunchConfigurationDelegate.getEnableLiveBeanSupport(wc)); + } + + public void testGetSetJMXPort() throws Exception { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + assertEquals("", BootLaunchConfigurationDelegate.getJMXPort(wc)); + + BootLaunchConfigurationDelegate.setJMXPort(wc, "something"); + assertEquals("something", BootLaunchConfigurationDelegate.getJMXPort(wc)); + } + + public void testRunAsLaunch() throws Exception { + IProject project = createLaunchReadyProject(TEST_PROJECT); + + //Creates a launch conf similar to that created by 'Run As' menu. + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + BootLaunchConfigurationDelegate.setDefaults(wc, project, TEST_MAIN_CLASS); + + LaunchResult result = LaunchUtil.synchLaunch(wc); + System.out.println(result); //Great help in debugging this :-) + assertContains(":: Spring Boot ::", result.out); + assertOk(result); + } + + public void testLaunchAllOptsDisable() throws Exception { + createLaunchReadyProject(TEST_PROJECT); + ILaunchConfigurationWorkingCopy wc = createBaseWorkingCopy(); + LaunchResult result = LaunchUtil.synchLaunch(wc); + assertContains(":: Spring Boot ::", result.out); + assertOk(result); + } + + public void testLaunchWithDebugOutput() throws Exception { + createLaunchReadyProject(TEST_PROJECT); + ILaunchConfigurationWorkingCopy wc = createBaseWorkingCopy(); + + BootLaunchConfigurationDelegate.setEnableDebugOutput(wc, true); + + LaunchResult result = LaunchUtil.synchLaunch(wc); + + assertContains(":: Spring Boot ::", result.out); + assertContains("AUTO-CONFIGURATION REPORT", result.out); + assertOk(result); + } + + public void testLaunchWithLiveBeans() throws Exception { + createLaunchReadyProject(TEST_PROJECT); + ILaunchConfigurationWorkingCopy wc = createBaseWorkingCopy(); + + BootLaunchConfigurationDelegate.setEnableLiveBeanSupport(wc, true); + int port = JmxBeanSupport.randomPort(); + BootLaunchConfigurationDelegate.setJMXPort(wc, ""+port); + + LaunchResult result = LaunchUtil.synchLaunch(wc); + + System.out.println(result); + + assertContains(":: Spring Boot ::", result.out); + //The following check doesn't real prove the live bean graph works, but at least it shows the VM args are + //taking effect. + assertContains("com.sun.management.jmxremote.port='"+port+"'", result.out); + assertOk(result); + } + + public void testLaunchWithProperties() throws Exception { + createLaunchReadyProject(TEST_PROJECT); + ILaunchConfigurationWorkingCopy wc = createBaseWorkingCopy(); + + BootLaunchConfigurationDelegate.setRawApplicationProperties(wc, + "foo=foo is enabled\n" + + "#bar=bar is not enabled\n" + + "zor=zor enabled\n" + + "#zor=zor disabled" + ); + + int jmxPort = JmxBeanSupport.randomPort(); // must set or it will be generated randomly + // and then we can't make the 'assert' below pass easily. + BootLaunchConfigurationDelegate.setJMXPort(wc, ""+jmxPort); + + LaunchResult result = LaunchUtil.synchLaunch(wc); + + assertContains(":: Spring Boot ::", result.out); + assertPropertyDump(result.out, + "debug=null\n" + + "zor='zor enabled'\n" + + "foo='foo is enabled'\n" + + "bar=null\n" + + "com.sun.management.jmxremote.port='"+jmxPort+"'" + ); + assertOk(result); + } + + public void testLaunchWithEmptyProperties() throws Exception { + createLaunchReadyProject(TEST_PROJECT); + ILaunchConfigurationWorkingCopy wc = createBaseWorkingCopy(); + + BootLaunchConfigurationDelegate.setRawApplicationProperties(wc, + "foo=\n" + + "#bar=bar is not enabled\n" + ); + + int jmxPort = JmxBeanSupport.randomPort(); // must set or it will be generated randomly + // and then we can't make the 'assert' below pass easily. + BootLaunchConfigurationDelegate.setJMXPort(wc, ""+jmxPort); + + LaunchResult result = LaunchUtil.synchLaunch(wc); + + assertContains(":: Spring Boot ::", result.out); + assertPropertyDump(result.out, + "debug=null\n" + + "zor=null\n" + + "foo=''\n" + + "bar=null\n" + + "com.sun.management.jmxremote.port='"+jmxPort+"'" + ); + assertOk(result); + } + + + public void testLaunchWithProfile() throws Exception { + createLaunchReadyProject(TEST_PROJECT); + ILaunchConfigurationWorkingCopy wc = createBaseWorkingCopy(); + + BootLaunchConfigurationDelegate.setProfile(wc, "special"); + + LaunchResult result = LaunchUtil.synchLaunch(wc); + + assertContains(":: Spring Boot ::", result.out); + assertContains("foo='special foo'", result.out); + assertOk(result); + } + + public void testLaunchWithThinWrapper() throws Exception { + BootProcessFactory.ENABLE_OUTPUT_DUMPING = true; + try { + URL thinWrapperUrl = new URL("https://repo1.maven.org/maven2/org/springframework/boot/experimental/spring-boot-thin-wrapper/1.0.9.RELEASE/spring-boot-thin-wrapper-1.0.9.RELEASE.jar"); + File thinWrapper = File.createTempFile("thin-wrapper", ".jar"); + try { + FileUtils.copyURLToFile(thinWrapperUrl, thinWrapper); + BootPreferences.getInstance().setThinWrapper(thinWrapper); + + doThinWrapperLaunchTest(thinWrapper, "MAVEN"); + //Not working: doThinWrapperLaunchTest(thinWrapper, "GRADLE"); + + } finally { + FileUtils.deleteQuietly(thinWrapper); + BootPreferences.getInstance().setThinWrapper(null); + } + } finally { + BootProcessFactory.ENABLE_OUTPUT_DUMPING = false; + } + } + + private void doThinWrapperLaunchTest(File thinWrapper, String importStrategy) throws Exception { + System.out.println(">>> doThinWrapperLaunchTest: "+importStrategy); + try { + boolean reallyDoThinLaunch = true; + Timewatch.monitor("Thin launch with "+importStrategy, Duration.ofMinutes(5), () -> { + String buildType = importStrategy.split("\\-")[0].toLowerCase(); + IProject project = projects.createBootProject("thinly-wrapped-"+buildType, withImportStrategy(importStrategy)); + ILaunchConfigurationWorkingCopy wc = createBaseWorkingCopy(project.getName(), "com.example.demo.ThinlyWrapped"+ StringUtils.capitalize(buildType) +"Application"); + + createFile(project, "src/main/java/com/example/demo/ShowMessage.java", + "package com.example.demo;\n" + + "\n" + + "import org.springframework.boot.CommandLineRunner;\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class ShowMessage implements CommandLineRunner {\n" + + "\n" + + " @Override\n" + + " public void run(String... arg0) throws Exception {\n" + + " System.out.println(\"We have liftoff!\");\n" + + " }\n" + + "\n" + + "}\n" + ); + StsTestUtil.assertNoErrors(project); // compile project (and check for errors) + if (buildType.equals("maven")) { + //In gradle its different location, but doens't really matter, this just a 'sanity' check to + // see if class got compiled. If not, something else will fail later. + assertTrue(project.getFile("target/classes/com/example/demo/ShowMessage.class").exists()); + } + + if (reallyDoThinLaunch) { + BootLaunchConfigurationDelegate.setUseThinWrapper(wc, true); + String[] classpath = getClasspath(new BootLaunchConfigurationDelegate(), wc); + assertTrue(classpath.length==1); + assertEquals(thinWrapper.getAbsolutePath(), classpath[0]); + } + + LaunchResult result = LaunchUtil.synchLaunch(wc); + System.out.println(result); + assertContains("We have liftoff!", result.out); + }); + } finally { + System.out.println("<<< doThinWrapperLaunchTest: "+importStrategy); + } + } + + /** + * Name fragments that, when seen in a dependency, probably mean the dependency is a test dependency + */ + private static final String[] testFragments = { + "test", + "junit", + "assertj", + "mockito", + }; + + private static boolean isTestClasspathEntry(String cpe) { + for (String testFrag : testFragments) { + String normalizedEntry = cpe.replace('\\', '/'); + int slash = normalizedEntry.lastIndexOf('/'); + if (slash>=0) { + normalizedEntry = normalizedEntry.substring(slash); + } + if (normalizedEntry.contains(testFrag)) { + return true; + } + } + return false; + } + + + public void testRuntimeClasspathNoTestStuffGradle() throws Exception { + IProject project = projects.createBootProject("gradle-test-project", withImportStrategy("GRADLE")); + ILaunchConfigurationWorkingCopy wc = createBaseWorkingCopy(project.getName(), "com.example.demo.GradleTestProjectApplication"); + String[] cp = getClasspath(new BootLaunchConfigurationDelegate(), wc); + + ArrayList testDependencies = new ArrayList<>(); + for (String cpe : cp) { + if (isTestClasspathEntry(cpe)) { + testDependencies.add(cpe); + } + } + if (!testDependencies.isEmpty()) { + fail("Shouldn't have test dependencies but found: "+testDependencies); + } + } + + public void testRuntimeClasspathNoTestStuff() throws Exception { + createLaunchReadyProject(TEST_PROJECT); + ILaunchConfigurationWorkingCopy wc = createBaseWorkingCopy(); + String[] cp = getClasspath(new BootLaunchConfigurationDelegate(), wc); + assertClasspath(cp, + //ignore: + (s) -> s.contains("/jre/lib/") || s.contains("/Java/Extensions/"), + //expect: + "target/classes", + "spring-boot-starter-1.2.1.RELEASE.jar", + "spring-boot-1.2.1.RELEASE.jar", + "spring-context-4.1.4.RELEASE.jar", + "spring-aop-4.1.4.RELEASE.jar", + "aopalliance-1.0.jar", + "spring-beans-4.1.4.RELEASE.jar", + "spring-expression-4.1.4.RELEASE.jar", + "spring-boot-autoconfigure-1.2.1.RELEASE.jar", + "spring-boot-starter-logging-1.2.1.RELEASE.jar", + "jcl-over-slf4j-1.7.8.jar", + "slf4j-api-1.7.8.jar", + "jul-to-slf4j-1.7.8.jar", + "log4j-over-slf4j-1.7.8.jar", + "logback-classic-1.1.2.jar", + "logback-core-1.1.2.jar", + "spring-core-4.1.4.RELEASE.jar", + "snakeyaml-1.14.jar" + ); + } + + private static void assertClasspath(String[] _cp, Predicate ignoring, String... expected) { + List cpList = new ArrayList(_cp.length); + for (int i = 0; i < _cp.length; i++) { + String normalizedEntry = _cp[i].replace('\\', '/'); + if (!ignoring.test(normalizedEntry)) { + cpList.add(normalizedEntry); + } + } + String[] cp = cpList.toArray(new String[cpList.size()]); + for (String e : expected) { + assertClasspathHasEntry(cp, e); + } + for (String e : cp) { + assertClasspathEntryExpected(e, expected); + } + + } + + private static void assertClasspathEntryExpected(String e, String[] expected) { + for (String expect : expected) { + if (e.endsWith(expect)) { + return; + } + } + fail("Unexpected classpath entry: "+e); + } + + private static void assertClasspathHasEntry(String[] cp, String expect) { + StringBuilder found = new StringBuilder(); + for (String actual : cp) { + found.append(actual+"\n"); + if (actual.endsWith(expect)) { + return; + } + } + fail("Missing classpath entry: "+expect+"\n" + + "found: "+found); + } + + private String[] getClasspath(JavaLaunchDelegate delegate, + ILaunchConfigurationWorkingCopy wc) throws CoreException { + System.out.println("\n====classpath according to "+delegate.getClass().getSimpleName()); + String[][] cpAndMp = delegate.getClasspathAndModulepath(wc); + String[] classpath = cpAndMp[0]; + for (String element : classpath) { + int chop = element.lastIndexOf('/'); + System.out.println('"'+element.substring(chop+1)+"\","); + } + return classpath; + } + + private void assertPropertyDump(String out, String expected) { + String BEG = ">>>properties"; + String END = "<< { + assertElements(findTypes(project), //something + "com.example.demo.KotlinBootappApplicationKt" + ); + }); + } + + public void testMainClassFindTypes() throws Exception { + IJavaProject project = JavaCore.create(createLaunchReadyProject(PROJECT)); + IType target = project.findType(ALT_MAIN_TYPE); + assertElements(findTypes(target), + ALT_MAIN_TYPE + ); + } + + public void testMainMethodFindTypes() throws Exception { + IJavaProject project = JavaCore.create(createLaunchReadyProject(PROJECT)); + IMethod target = getMainMethod(project.findType(ALT_MAIN_TYPE)); + assertElements(findTypes(target), + ALT_MAIN_TYPE + ); + } + + public void testMainClassSourceFileFindTypes() throws Exception { + IJavaProject project = JavaCore.create(createLaunchReadyProject(PROJECT)); + IResource target = project.findType(ALT_MAIN_TYPE).getUnderlyingResource(); + assertElements(findTypes(target), + ALT_MAIN_TYPE + ); + } + + public void testMainClassCompilationUnitFindTypes() throws Exception { + IJavaProject project = JavaCore.create(createLaunchReadyProject(PROJECT)); + ICompilationUnit target = JavaCore.createCompilationUnitFrom( + (IFile) project.findType(ALT_MAIN_TYPE).getUnderlyingResource()); + assertElements(findTypes(target), + ALT_MAIN_TYPE + ); + } + + public void testCreateConfiguration() throws Exception { + IJavaProject project = JavaCore.create(createLaunchReadyProject(PROJECT)); + IType mainType = project.findType(MAIN_TYPE); + ILaunchConfiguration conf = shortcut.createConfiguration(mainType); + + assertEquals(getProject(PROJECT), BootLaunchConfigurationDelegate.getProject(conf)); + assertEquals(MAIN_TYPE, + conf.getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, "")); + assertEquals("", getProfile(conf)); + assertEquals(DEFAULT_ENABLE_DEBUG_OUTPUT, getEnableDebugOutput(conf)); + assertEquals(DEFAULT_ENABLE_LIVE_BEAN_SUPPORT(), getEnableLiveBeanSupport(conf)); + int port = Integer.parseInt(getJMXPort(conf)); + assertEquals(0, port); // 0 means 'allocate dynamically' + assertEquals("", getRawApplicationProperties(conf)); + } + + public void testConfigurationDeletedWhenProjectDeleted() throws Exception { + IJavaProject project = JavaCore.create(createLaunchReadyProject(PROJECT)); + IType mainType = project.findType(MAIN_TYPE); + final ILaunchConfiguration conf = shortcut.createConfiguration(mainType); + + assertTrue(conf.exists()); + + project.getProject().delete(true, true, new NullProgressMonitor()); + //The delete fo launch conf happens in response to project delete, but + // its in a job so we don't know exactly when it will be done... so... + new ACondition() { + @Override + public boolean test() throws Exception { + assertFalse(conf.exists()); + return true; + } + }.waitFor(5000); + } + + public void testLaunch() throws Exception { + IJavaProject project = JavaCore.create(createLaunchReadyProject(PROJECT)); + IType mainType = project.findType(MAIN_TYPE); + ILaunchConfiguration conf = shortcut.createConfiguration(mainType); + LaunchResult r = LaunchUtil.synchLaunch(conf); + assertContains(":: Spring Boot ::", r.out); + assertOk(r); + } + + //////////////////////////////////////////////////////////////// + + private IMethod getMainMethod(IType type) throws Exception { + for (IMethod m : type.getMethods()) { + if ("main".equals(m.getElementName())) { + return m; + } + } + throw new Error("No main method in "+type); + } + + public String[] findTypes(Object selection) throws InterruptedException, CoreException { + IType[] types = shortcut.findTypes(new Object[] {selection}, testContext); + if (types!=null && types.length>0) { + String[] names = new String[types.length]; + for (int i = 0; i < names.length; i++) { + names[i] = types[i].getFullyQualifiedName(); + } + return names; + } + return new String[0]; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootLaunchTestCase.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootLaunchTestCase.java new file mode 100644 index 000000000..a6f9c52c6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootLaunchTestCase.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch.test; + +import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.withLanguage; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; +import org.springframework.ide.eclipse.boot.test.util.LaunchResult; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.tests.util.StsTestCase; +import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; + +/** + * @author Kris De Volder + */ +public class BootLaunchTestCase extends StsTestCase { + + protected BootProjectTestHarness projects = new BootProjectTestHarness(ResourcesPlugin.getWorkspace()); + + /** + * Create an empty project no nature, no nothing + */ + public static IProject createGeneralProject(String name) throws Exception { + IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(name); + p.create(new NullProgressMonitor()); + p.open(new NullProgressMonitor()); + assertTrue(p.exists()); + assertTrue(p.isAccessible()); + return p; + } + + public static void assertOk(LaunchResult result) { + assertEquals(0, result.terminationCode); + } + + @Override + protected String getBundleName() { + return "org.springframework.ide.eclipse.boot.launch.test"; + } + + protected ILaunchConfigurationWorkingCopy createWorkingCopy(String launchConfTypeId) throws CoreException { + String name = DebugPlugin.getDefault().getLaunchManager().generateLaunchConfigurationName("test"); + ILaunchConfigurationWorkingCopy wc = DebugPlugin.getDefault().getLaunchManager() + .getLaunchConfigurationType(launchConfTypeId) + .newInstance(null, name); + return wc; + } + + public IProject getProject(String name) { + return ResourcesPlugin.getWorkspace().getRoot().getProject(name); + } + + public void assertError(String snippet, LiveExpression validator) { + ValidationResult value = validator.getValue(); + assertEquals(IStatus.ERROR, value.status); + assertContains(snippet, value.msg); + } + + public static void assertOk(LiveExpression validator) { + ValidationResult status = validator.getValue(); + if (!status.isOk()) { + fail(status.toString()); + } + } + + /** + * Tests that want to launch something from a project should use this rather to + * make sure project is built and has no errors. + *

+ * Projects with errors shouldn't be launched as they will just cause launcher tests to + * fail in confusing and unpredictabled ways. + */ + public IProject createLaunchReadyProject(String projectName) throws Exception { + IProject project = createPredefinedProject(projectName); + StsTestUtil.assertNoErrors(project); + return project; + } + + public IJavaProject createLaunchReadyKotlinProject(final String projectName) throws Exception { + return JavaCore.create(projects.createBootWebProject(projectName, withLanguage("kotlin"))); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootLaunchUIModelTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootLaunchUIModelTest.java new file mode 100644 index 000000000..f1b2c773a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/BootLaunchUIModelTest.java @@ -0,0 +1,564 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch.test; + +import static org.junit.Assert.assertArrayEquals; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.springframework.ide.eclipse.boot.launch.BootLaunchActivator; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.BootLaunchUIModel; +import org.springframework.ide.eclipse.boot.launch.IProfileHistory; +import org.springframework.ide.eclipse.boot.launch.LaunchTabSelectionModel; +import org.springframework.ide.eclipse.boot.launch.MainTypeNameLaunchTabModel; +import org.springframework.ide.eclipse.boot.launch.ProfileLaunchTabModel; +import org.springframework.ide.eclipse.boot.launch.SelectProjectLaunchTabModel; +import org.springframework.ide.eclipse.boot.launch.livebean.EnableJmxFeaturesModel; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; + +/** + * @author Kris De Volder + */ +public class BootLaunchUIModelTest extends BootLaunchTestCase { + + private static final String[] NO_PROFILES = new String[0]; + + public class TestProfileHistory implements IProfileHistory { + + private Map map = new HashMap<>(); + + @Override + public String[] getHistory(IProject project) { + String[] h = map.get(project.getName()); + if (h!=null) { + return h; + } + return NO_PROFILES; + } + + public void setHistory(IProject p, String... history) { + map.put(p.getName(), history); + } + + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + protected BootLaunchUIModel model; + protected TestProfileHistory profileHistory; + + @Override + protected void setUp() throws Exception { + super.setUp(); + profileHistory = new TestProfileHistory(); + model = new BootLaunchUIModel(profileHistory); + } + + ///// project //////////////////////////////////////////////////////////// + + public void testProjectValidator() throws Exception { + createPredefinedProject("empty-boot-project"); + createGeneralProject("general"); + assertError("No project selected", model.project.validator); + + model.project.selection.setValue(getProject("non-existant")); + assertError("does not exist", model.project.validator); + + model.project.selection.setValue(getProject("general")); + assertError("does not look like a Boot project", model.project.validator); + + model.project.selection.setValue(getProject("empty-boot-project")); + assertOk(model.project.validator); + + getProject("empty-boot-project").close(new NullProgressMonitor()); + model.project.validator.refresh(); //manual refresh is needed + // no auto refresh when closing project. This is normal + // and it is okay since user can't open/close projects + // while using launch config dialog. + assertError("is closed", model.project.validator); + } + + private ILaunchConfigurationWorkingCopy createWorkingCopy() throws CoreException { + return createWorkingCopy(BootLaunchConfigurationDelegate.TYPE_ID); + } + + public void testProjectInitializeFrom() throws Exception { + IProject fooProject = getProject("foo"); + + SelectProjectLaunchTabModel project = model.project; + LiveVariable dirtyState = model.project.getDirtyState(); + + dirtyState.setValue(false); + project.selection.setValue(fooProject); + assertTrue(dirtyState.getValue()); + + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + project.initializeFrom(wc); + assertEquals(null, project.selection.getValue()); + assertFalse(dirtyState.getValue()); + + BootLaunchConfigurationDelegate.setProject(wc, fooProject); + project.initializeFrom(wc); + assertEquals(fooProject, project.selection.getValue()); + } + + public void testProjectPerformApply() throws Exception { + IProject fooProject = getProject("foo"); + + SelectProjectLaunchTabModel project = model.project; + LiveVariable dirtyState = model.project.getDirtyState(); + + dirtyState.setValue(false); + project.selection.setValue(fooProject); + assertTrue(dirtyState.getValue()); + + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + assertEquals(null, BootLaunchConfigurationDelegate.getProject(wc)); + + project.performApply(wc); + + assertFalse(dirtyState.getValue()); + assertEquals(fooProject, BootLaunchConfigurationDelegate.getProject(wc)); + } + + public void testProjectSetDefaults() throws Exception { + IProject fooProject = getProject("foo"); + + SelectProjectLaunchTabModel project = model.project; + + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + BootLaunchConfigurationDelegate.setProject(wc, fooProject); + assertEquals(fooProject, BootLaunchConfigurationDelegate.getProject(wc)); + project.setDefaults(wc); + assertEquals(null, BootLaunchConfigurationDelegate.getProject(wc)); + } + + public void testProjectDirtyState() throws Exception { + SelectProjectLaunchTabModel project = model.project; + LiveVariable dirtyState = model.project.getDirtyState(); + + dirtyState.setValue(false); + project.selection.setValue(getProject("nono")); + assertTrue(dirtyState.getValue()); + } + + ////// main type ////////////////////////////////////////////////////////// + + public void testMainTypeValidator() throws Exception { + assertEquals("", model.mainTypeName.selection.getValue()); + assertError("No Main type selected", model.mainTypeName.validator); + model.mainTypeName.selection.setValue("something"); + assertOk(model.mainTypeName.validator); + } + + public void testMainTypeInitializeFrom() throws Exception { + MainTypeNameLaunchTabModel mainTypeName = model.mainTypeName; + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + LiveVariable dirtyState = model.mainTypeName.getDirtyState(); + + BootLaunchConfigurationDelegate.setMainType(wc, "Snuggem"); + dirtyState.setValue(true); + + mainTypeName.initializeFrom(wc); + + assertFalse(dirtyState.getValue()); + assertEquals("Snuggem", mainTypeName.selection.getValue()); + } + + public void testMainTypePerformApply() throws Exception { + MainTypeNameLaunchTabModel mainTypeName = model.mainTypeName; + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + LiveVariable dirtyState = model.mainTypeName.getDirtyState(); + + mainTypeName.selection.setValue("Koko"); + assertTrue(dirtyState.getValue()); + + mainTypeName.performApply(wc); + + assertEquals("Koko", getMainTypeName(wc)); + assertFalse(dirtyState.getValue()); + } + + public void testMainTypeSetDefaults() throws Exception { + MainTypeNameLaunchTabModel mainTypeName = model.mainTypeName; + + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + mainTypeName.setDefaults(wc); + assertEquals("", getMainTypeName(wc)); + } + + protected String getMainTypeName(ILaunchConfigurationWorkingCopy wc) + throws CoreException { + return wc.getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, (String)null); + } + + public void testMainTypeDirtyState() throws Exception { + MainTypeNameLaunchTabModel mainTypeName = model.mainTypeName; + LiveVariable dirtyState = model.mainTypeName.getDirtyState(); + + dirtyState.setValue(false); + mainTypeName.selection.setValue("something"); + assertTrue(dirtyState.getValue()); + } + + ////// profile //////////////////////////////////////////////////////////////////// + + public void testProfileValidator() throws Exception { + assertEquals(Validator.OK, model.profile.validator); + //not much to test here, we don't validate profiles at all. + } + + public void testProfileSetDefaults() throws Exception { + ProfileLaunchTabModel profile = model.profile; + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + profile.setDefaults(wc); + assertEquals("", BootLaunchConfigurationDelegate.getProfile(wc)); + } + + public void testProfileInitializeFrom() throws Exception { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + ProfileLaunchTabModel profile = model.profile; + LiveVariable dirty = profile.getDirtyState(); + + dirty.setValue(true); + BootLaunchConfigurationDelegate.setProfile(wc,"some-profile"); + profile.initializeFrom(wc); + assertFalse(dirty.getValue()); + assertEquals("some-profile", BootLaunchConfigurationDelegate.getProfile(wc)); + } + + public void testProfilePerformApply() throws Exception { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + ProfileLaunchTabModel profile = model.profile; + LiveVariable dirty = profile.getDirtyState(); + + profile.selection.setValue("some-other-profile"); + assertTrue(dirty.getValue()); + + profile.performApply(wc); + + assertFalse(dirty.getValue()); + assertEquals("some-other-profile", BootLaunchConfigurationDelegate.getProfile(wc)); + } + + public void testProfileDirtyState() throws Exception { + ProfileLaunchTabModel profile = model.profile; + LiveVariable dirty = profile.getDirtyState(); + dirty.setValue(false); + + profile.selection.setValue("olla-polla"); + + assertTrue(dirty.getValue()); + } + + public void testProfilePulldownOptions() throws Exception { + IProject bootProject = createPredefinedProject("empty-boot-project"); + IProject generalProject = createGeneralProject("general"); + + LiveVariable project = model.project.selection; + ProfileLaunchTabModel profile = model.profile; + + assertPulldown(profile /*empty*/); + + createEmptyFile(bootProject, "src/main/resources/application-foo.properties"); + createEmptyFile(bootProject, "src/main/resources/application-bar.properties"); + + project.setValue(bootProject); + assertPulldown(profile, "foo", "bar"); + + project.setValue(getProject("invalid")); + assertPulldown(profile /*empty*/); + + profileHistory.setHistory(generalProject, "something", "borker"); + project.setValue(generalProject); + assertPulldown(profile, "something", "borker"); + + profileHistory.setHistory(bootProject, "old", "older"); + project.setValue(bootProject); + assertPulldown(profile, "foo", "bar", "old", "older"); + + profileHistory.setHistory(bootProject, "new", "newer", "foo"); + profile.profileOptions().refresh(); // See [*] below + assertPulldown(profile, "foo", "bar", "new", "newer"); //only a single 'foo'! + + // [*] Changing only the history doesn't trigger pull-down options to recompute. + //This is fine since its not possible to change the history while launch dialog is open. + //However, it means test code must force a refresh in cases where only the history + //changed. + } + + ///// EnableDebugSection/////////////////////////////////////////////////////// + + public void testDebugValidator() throws Exception { + assertEquals(Validator.OK, model.enableDebug.validator); + } + + public void testDebugSetDefaults() throws Exception { + LaunchTabSelectionModel enableDebug = model.enableDebug; + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + enableDebug.setDefaults(wc); + assertEquals(false, BootLaunchConfigurationDelegate.getEnableDebugOutput(wc)); + } + + public void testDebugInitializeFrom() throws Exception { + LaunchTabSelectionModel enableDebug = model.enableDebug; + LiveVariable dirty = enableDebug.getDirtyState(); + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + + BootLaunchConfigurationDelegate.setEnableDebugOutput(wc, true); + dirty.setValue(true); + + enableDebug.initializeFrom(wc); + assertFalse(dirty.getValue()); + assertTrue(enableDebug.selection.getValue()); + + dirty.setValue(true); + BootLaunchConfigurationDelegate.setEnableDebugOutput(wc, false); + + enableDebug.initializeFrom(wc); + + assertFalse(dirty.getValue()); + assertFalse(enableDebug.selection.getValue()); + } + + public void testDebugPerformApply() throws Exception { + LaunchTabSelectionModel enableDebug = model.enableDebug; + LiveVariable dirty = enableDebug.getDirtyState(); + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + + enableDebug.selection.setValue(true); + assertTrue(dirty.getValue()); + + enableDebug.performApply(wc); + + assertFalse(dirty.getValue()); + assertTrue(BootLaunchConfigurationDelegate.getEnableDebugOutput(wc)); + + enableDebug.selection.setValue(false); + assertTrue(dirty.getValue()); + + enableDebug.performApply(wc); + + assertFalse(dirty.getValue()); + assertFalse(BootLaunchConfigurationDelegate.getEnableDebugOutput(wc)); + } + + public void testDebugDirtyState() throws Exception { + LaunchTabSelectionModel enableDebug = model.enableDebug; + LiveVariable dirty = enableDebug.getDirtyState(); + dirty.setValue(false); + + enableDebug.selection.setValue(true); + + assertTrue(dirty.getValue()); + } + + ///// EnableJmxSectiom ///////////////////////////////////////////// + + public void testJmxValidator() throws Exception { + EnableJmxFeaturesModel eJmxModel = this.model.enableJmx; + LiveVariable jmx = eJmxModel.jmxEnabled; + LiveVariable liveBean = eJmxModel.liveBeanEnabled; + LiveVariable lifeCycle = eJmxModel.lifeCycleEnabled; + LiveVariable port = eJmxModel.port; + LiveVariable timeout = eJmxModel.terminationTimeout; + LiveExpression validator = eJmxModel.getValidator(); + + liveBean.setValue(true); + lifeCycle.setValue(true); + timeout.setValue("5000"); + port.setValue("8888"); + assertOk(validator); + + port.setValue("Unparseable"); + assertError("can't be parsed as an Integer", validator); + + jmx.setValue(false); + liveBean.setValue(false); + lifeCycle.setValue(false); + assertOk(validator); //if disabled we shouldn't check the port as it doesn't matter. + + jmx.setValue(true); + port.setValue("10000000"); + liveBean.setValue(true); + assertError("should be smaller than", validator); + + jmx.setValue(false); + liveBean.setValue(false); + port.setValue("-111"); + assertOk(validator); //if disabled we shouldn't check the port as it doesn't matter. + + jmx.setValue(true); + lifeCycle.setValue(true); //only enable lifeCycle + assertFalse(liveBean.getValue()); + assertError("should be a positive", validator); + + port.setValue("0"); + assertOk(validator); // 0 is acceptable as it means 'choose dynamically'. + + port.setValue(" 8888 "); + assertOk(validator); // tolerate spaces + + port.setValue(null); // + assertError("JMX Port must be specified", validator); + + port.setValue(" "); // + assertError("JMX Port must be specified", validator); + + port.setValue(""); + assertError("JMX Port must be specified", validator); + + port.setValue("8888"); + assertOk(validator); + + timeout.setValue(null); + assertError("Termination timeout must be specified", validator); + + timeout.setValue(""); + assertError("Termination timeout must be specified", validator); + + timeout.setValue(" "); + assertError("Termination timeout must be specified", validator); + + timeout.setValue(" 8888 "); // tolerate spaces + assertOk(validator); + + timeout.setValue(" -555"); + assertError("Termination timeout must be positive", validator); + + timeout.setValue("abc"); + assertError("Termination timeout can't be parsed as an Integer", validator); + } + + public void testJmxSetDefaults() throws Exception { + EnableJmxFeaturesModel eJmxModel = this.model.enableJmx; + + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + eJmxModel.setDefaults(wc); + assertEquals(BootLaunchActivator.getInstance().isLiveBeanSupported(), BootLaunchConfigurationDelegate.getEnableLiveBeanSupport(wc)); + assertTrue(BootLaunchConfigurationDelegate.getEnableLifeCycle(wc)); + int jmxPort = Integer.parseInt(BootLaunchConfigurationDelegate.getJMXPort(wc)); + assertEquals(BootLaunchConfigurationDelegate.DEFAULT_JMX_PORT, jmxPort); + } + + public void testJmxInitializeFrom() throws Exception { + EnableJmxFeaturesModel eJmxModel = this.model.enableJmx; + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + LiveVariable liveBean = eJmxModel.liveBeanEnabled; + LiveVariable lifeCycle = eJmxModel.lifeCycleEnabled; + LiveVariable port = eJmxModel.port; + LiveVariable dirty = eJmxModel.getDirtyState(); + + boolean[] bools = {true, false}; + for (boolean b1 : bools) { + for (boolean b2 : bools) { + BootLaunchConfigurationDelegate.setEnableLiveBeanSupport(wc, b1); + BootLaunchConfigurationDelegate.setEnableLifeCycle(wc, b2); + BootLaunchConfigurationDelegate.setJMXPort(wc, "3456"); + dirty.setValue(true); + + eJmxModel.initializeFrom(wc); + + assertFalse(dirty.getValue()); + assertEquals("3456", port.getValue()); + assertEquals(b1, (boolean)liveBean.getValue()); + assertEquals(b2, (boolean)lifeCycle.getValue()); + } + } + } + + public void testJmxPerformApply() throws Exception { + EnableJmxFeaturesModel eJmxModel = this.model.enableJmx; + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(); + + LiveVariable liveBean = eJmxModel.liveBeanEnabled; + LiveVariable lifeCycle = eJmxModel.lifeCycleEnabled; + LiveVariable port = eJmxModel.port; + LiveVariable dirty = eJmxModel.getDirtyState(); + + port.setValue("1234"); + + boolean[] bools = {true, false}; + for (boolean b1 : bools) { + for (boolean b2 : bools) { + liveBean.setValue(b1); + lifeCycle.setValue(b2); + assertTrue(dirty.getValue()); + + eJmxModel.performApply(wc); + + assertFalse(dirty.getValue()); + assertEquals("1234", BootLaunchConfigurationDelegate.getJMXPort(wc)); + assertEquals(b1, BootLaunchConfigurationDelegate.getEnableLiveBeanSupport(wc)); + assertEquals(b2, BootLaunchConfigurationDelegate.getEnableLifeCycle(wc)); + } + } + + //port + port.setValue(" 8888 "); + assertTrue(dirty.getValue()); + eJmxModel.performApply(wc); + assertFalse(dirty.getValue()); + assertEquals("8888", BootLaunchConfigurationDelegate.getJMXPort(wc)); + } + + public void testLiveBeanDirtyState() throws Exception { + EnableJmxFeaturesModel eJmxModel = this.model.enableJmx; + + LiveVariable liveBean = eJmxModel.liveBeanEnabled; + LiveVariable lifeCycle = eJmxModel.lifeCycleEnabled; + LiveVariable port = eJmxModel.port; + LiveVariable dirty = eJmxModel.getDirtyState(); + + dirty.setValue(false); + port.setValue("something"); + assertTrue(dirty.getValue()); + + dirty.setValue(false); + liveBean.setValue(!liveBean.getValue()); + assertTrue(dirty.getValue()); + + dirty.setValue(false); + lifeCycle.setValue(!lifeCycle.getValue()); + assertTrue(dirty.getValue()); + } + + ///// PropertiesTableSection ??? can't be tested in its current form (no separate 'model' to test) + + + /** + * Verify contents of 'pulldown' menu. This ignores the order of the elements + * because discovered elements may come in different orders... but does not + * ignore when there are duplicates. + */ + private void assertPulldown(ProfileLaunchTabModel profile, String... expecteds) { + Arrays.sort(expecteds); + String [] actuals = profile.profileOptions().getValue(); + actuals = Arrays.copyOf(actuals, actuals.length); + Arrays.sort(actuals); + assertArrayEquals(expecteds, actuals); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/ProfileHistoryTest.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/ProfileHistoryTest.java new file mode 100644 index 000000000..22406da24 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/src/org/springframework/ide/eclipse/boot/launch/test/ProfileHistoryTest.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch.test; + +import org.eclipse.core.resources.IProject; + +import org.springframework.ide.eclipse.boot.launch.profiles.ProfileHistory; + +/** + * @author Kris De Volder + */ +public class ProfileHistoryTest extends BootLaunchTestCase { + + private ProfileHistory history; + + protected void setUp() throws Exception { + super.setUp(); + history = new ProfileHistory(); + } + + public void testSimpleHistory() throws Exception { + IProject project = createGeneralProject("foo"); + assertHistory(project /*empty*/); + + history.updateHistory(project, "production"); + assertHistory(project, + "production" + ); + + history.updateHistory(project, "development"); + assertHistory(project, + "development", + "production" + ); + + history.updateHistory(project, "experimental"); + assertHistory(project, + "experimental", + "development", + "production" + ); + } + + public void testMultiAddSame() throws Exception { + IProject project = createGeneralProject("foo"); + assertHistory(project /*empty*/); + + for (int i = 0; i < 10; i++) { + history.updateHistory(project, "foo"); + assertHistory(project, "foo"); + } + } + + public void testExistingMovesToTop() throws Exception { + IProject project = createGeneralProject("foo"); + assertHistory(project /*empty*/); + + history.updateHistory(project, "production"); + history.updateHistory(project, "development"); + history.updateHistory(project, "experimental"); + assertHistory(project, + "experimental", + "development", + "production" + ); + + history.updateHistory(project, "production"); + assertHistory(project, + "production", + "experimental", + "development" + ); + + history.updateHistory(project, "production"); + assertHistory(project, + "production", + "experimental", + "development" + ); + + history.updateHistory(project, "experimental"); + assertHistory(project, + "experimental", + "production", + "development" + ); + } + + public void testOverflow() throws Exception { + IProject project = createGeneralProject("foo"); + int maxHist = 4; + history.setMaxHistory(maxHist); + for (int i = 1; i <= 10; i++) { + history.updateHistory(project, "prof-"+i); + if (i>maxHist) { + String[] expected = new String[maxHist]; + for (int j = 0; j < expected.length; j++) { + expected[j] = "prof-"+(i-j); + } + assertHistory(project, expected); + } + } + } + + private void assertHistory(IProject project, String... expecteds) { + String[] actuals = history.getHistory(project); + StringBuilder actualStr = new StringBuilder(); + StringBuilder expectStr = new StringBuilder(); + for (String string : actuals) { + actualStr.append(string+"\n"); + } + for (String string : expecteds) { + expectStr.append(string+"\n"); + } + assertEquals(expectStr.toString(), actualStr.toString()); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.classpath new file mode 100644 index 000000000..5e8d85320 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.project new file mode 100644 index 000000000..9547f3d87 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.project @@ -0,0 +1,35 @@ + + + dump-info + + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.wst.common.project.facet.core.nature + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.settings/org.eclipse.core.resources.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..839d647ee --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..ec4300d5d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.settings/org.eclipse.m2e.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 000000000..96b30f980 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles=pom.xml +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/pom.xml new file mode 100644 index 000000000..4535a429e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.test + dump-info + 0.0.1-SNAPSHOT + jar + + dump-info + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 1.2.1.RELEASE + + + + + UTF-8 + demo.DumpInfoApplication + 1.7 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/src/main/java/demo/DumpInfoApplication.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/src/main/java/demo/DumpInfoApplication.java new file mode 100644 index 000000000..f4be8b889 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/dump-info/src/main/java/demo/DumpInfoApplication.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2015 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package demo; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.core.env.Environment; + +/** + * Simple Spring Boot application used by STS regression tests. The application + * prints out some info onto System.out and exits. + *

+ * The test harness uses the output to verify whether launch parameters passed via + * STS BootLaunchconfiguration produce the expected result. + * + * @author Kris De Volder + */ +@SpringBootApplication +public class DumpInfoApplication implements CommandLineRunner { + + public static String[] INTERESTING_PROPERTIES = { + "debug", + "zor", + "foo", + "bar", + "com.sun.management.jmxremote.port" + }; + + public static void main(String[] args) { + SpringApplication.run(DumpInfoApplication.class, args); + } + + @Autowired + Environment properties; + + @Override + public void run(String... args) throws Exception { + System.out.println(">>>properties"); + for (String prop : INTERESTING_PROPERTIES) { + System.out.println(prop+"="+render(properties.getProperty(prop))); + } + System.out.println("<< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.project new file mode 100644 index 000000000..35324fa84 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.project @@ -0,0 +1,35 @@ + + + empty-boot-project + + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.wst.common.project.facet.core.nature + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.settings/org.eclipse.core.resources.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..839d647ee --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..ec4300d5d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.settings/org.eclipse.m2e.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 000000000..96b30f980 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles=pom.xml +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/pom.xml new file mode 100644 index 000000000..083ccf8d5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.test + empty-boot-project + 0.0.1-SNAPSHOT + jar + + empty-boot-project + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 1.2.1.RELEASE + + + + + UTF-8 + demo.EmptyBootProjectApplication + 1.7 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/src/main/java/demo/AlternateMain.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/src/main/java/demo/AlternateMain.java new file mode 100644 index 000000000..e1a2386ab --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/src/main/java/demo/AlternateMain.java @@ -0,0 +1,12 @@ +package demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AlternateMain { + + public static void main(String[] args) { + SpringApplication.run(AlternateMain.class, args); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/src/main/java/demo/EmptyBootProjectApplication.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/src/main/java/demo/EmptyBootProjectApplication.java new file mode 100644 index 000000000..9e3ad63d6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/src/main/java/demo/EmptyBootProjectApplication.java @@ -0,0 +1,12 @@ +package demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class EmptyBootProjectApplication { + + public static void main(String[] args) { + SpringApplication.run(EmptyBootProjectApplication.class, args); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/src/main/resources/application.properties b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/src/main/resources/application.properties new file mode 100644 index 000000000..e69de29bb diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/src/test/java/demo/EmptyBootProjectApplicationTests.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/src/test/java/demo/EmptyBootProjectApplicationTests.java new file mode 100644 index 000000000..a421539c5 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/empty-boot-project/src/test/java/demo/EmptyBootProjectApplicationTests.java @@ -0,0 +1,16 @@ +package demo; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = EmptyBootProjectApplication.class) +public class EmptyBootProjectApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.classpath new file mode 100644 index 000000000..5e8d85320 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.project new file mode 100644 index 000000000..05ce0b784 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.project @@ -0,0 +1,35 @@ + + + project with spaces + + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.wst.common.project.facet.core.nature + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.settings/org.eclipse.core.resources.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..839d647ee --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..ec4300d5d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.settings/org.eclipse.m2e.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 000000000..96b30f980 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles=pom.xml +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/pom.xml new file mode 100644 index 000000000..17567d283 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.test + dump-info + 0.0.1-SNAPSHOT + jar + + project with spaces + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 1.2.1.RELEASE + + + + + UTF-8 + demo.DumpInfoApplication + 1.7 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/src/main/java/demo/DumpInfoApplication.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/src/main/java/demo/DumpInfoApplication.java new file mode 100644 index 000000000..f4be8b889 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch.test/workspace/project with spaces/src/main/java/demo/DumpInfoApplication.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2015 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package demo; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.core.env.Environment; + +/** + * Simple Spring Boot application used by STS regression tests. The application + * prints out some info onto System.out and exits. + *

+ * The test harness uses the output to verify whether launch parameters passed via + * STS BootLaunchconfiguration produce the expected result. + * + * @author Kris De Volder + */ +@SpringBootApplication +public class DumpInfoApplication implements CommandLineRunner { + + public static String[] INTERESTING_PROPERTIES = { + "debug", + "zor", + "foo", + "bar", + "com.sun.management.jmxremote.port" + }; + + public static void main(String[] args) { + SpringApplication.run(DumpInfoApplication.class, args); + } + + @Autowired + Environment properties; + + @Override + public void run(String... args) throws Exception { + System.out.println(">>>properties"); + for (String prop : INTERESTING_PROPERTIES) { + System.out.println(prop+"="+render(properties.getProperty(prop))); + } + System.out.println("<< + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.project b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.project new file mode 100644 index 000000000..d4881d4ec --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.project @@ -0,0 +1,39 @@ + + + org.springframework.ide.eclipse.boot.launch + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + + + 1421885564492 + + 10 + + org.eclipse.ui.ide.multiFilter + 1.0-projectRelativePath-matches-false-false-target + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..0c68a61dc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.settings/org.eclipse.jdt.ui.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..a15aefcfd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,60 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=false +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=false +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_type_arguments=true +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=false +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true +sp_cleanup.use_type_arguments=false diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/META-INF/MANIFEST.MF b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/META-INF/MANIFEST.MF new file mode 100644 index 000000000..e5af06eed --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/META-INF/MANIFEST.MF @@ -0,0 +1,44 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Boot Launch +Bundle-SymbolicName: org.springframework.ide.eclipse.boot.launch;singleton:=true +Bundle-Vendor: Pivotal Software Inc. +Bundle-Version: 4.8.1.qualifier +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.debug.core, + org.eclipse.jdt.core, + org.eclipse.jdt.debug.ui, + org.eclipse.jdt.launching, + org.springsource.ide.eclipse.commons.frameworks.core, + org.eclipse.debug.ui, + org.springsource.ide.eclipse.commons.livexp;bundle-version="3.8.4", + org.springframework.ide.eclipse.boot, + org.eclipse.core.expressions, + org.springsource.ide.eclipse.commons.frameworks.ui, + org.springsource.ide.eclipse.commons.ui, + org.springsource.ide.eclipse.commons.core, + org.eclipse.ui.console, + org.eclipse.m2e.jdt, + com.google.guava, + org.springframework.ide.eclipse.editor.support, + org.eclipse.jdt.debug, + org.eclipse.ui.workbench.texteditor, + org.eclipse.ui.editors, + org.eclipse.jface.text, + org.eclipse.jdt.ui, + org.eclipse.ui.genericeditor, + org.eclipse.ui.ide +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Export-Package: org.springframework.ide.eclipse.boot.launch, + org.springframework.ide.eclipse.boot.launch.cli, + org.springframework.ide.eclipse.boot.launch.devtools, + org.springframework.ide.eclipse.boot.launch.livebean, + org.springframework.ide.eclipse.boot.launch.process, + org.springframework.ide.eclipse.boot.launch.profiles, + org.springframework.ide.eclipse.boot.launch.properties, + org.springframework.ide.eclipse.boot.launch.util +Bundle-Activator: org.springframework.ide.eclipse.boot.launch.BootLaunchActivator +Automatic-Module-Name: org.springframework.ide.eclipse.boot.launch diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/about.html b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/about.html new file mode 100644 index 000000000..55dfadc68 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/about.html @@ -0,0 +1,248 @@ + + + + +About Spring IDE + + + +

About Spring IDE

+

March 26, 2007

+

Abstract

+

Spring IDE is a set of plugins which provide a user interface for +The Spring Framework's bean +factory XML configuration files including support for Spring 2.0 +namespaces, AOP configuration and Spring Web Flow.

+

License

+ +

The Spring IDE Project makes available all content in this +plug-in ("Content"). Unless otherwise indicated below, the +Content is provided to you under the terms and conditions of the Eclipse +Public License Version 1.0 ("EPL"). A copy of the EPL is +available at https://www.eclipse.org/legal/epl-v10.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the Spring IDE +Project, the Content is being redistributed by another party +("Redistributor") and different terms and conditions may apply +to your use of any object code in the Content. Check the Redistributor's +license that was provided with the Content. If no such license exists, +contact the Redistributor. Unless otherwise indicated below, the terms +and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at https://www.eclipse.org.

+ +

Third Party Content

+

The Content includes items that have been sourced from third +parties as set out below. If you did not receive this Content directly +from the Spring IDE Project, the following is provided for informational +purposes only, and you should look to the Redistributor's license for +terms and conditions of use.

+

The Spring Framework v3.0.5 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Web Flow v2.0.7.A +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Web Services v1.5.8 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Security v3.0.2 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Dynamic Modules for OSGi(TM) Runtimes v2.0.0.M2 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Spring Batch v2.1.0 +

This product includes software developed by the Spring Framework +Project (https://www.springframework.org).

+ +

Commons Codec v1.3.0 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

Commons Collections v3.2.0 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

Commons Logging v1.1.1 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

Apache MyFaces v1.2.2 +

This product includes software developed by The Apache Software +Foundation (https://www.apache.org/).

+ +

AspectJ Weaver v1.6.10 +

This product includes software developed by The Eclipse Software +Foundation (https://www.eclipse.org/).

+ +

Antlr v3.0.1 +

Spring IDE includes a binary version of Antlr v3.0.1 (https://www.antlr.org/). +The source code for Antlr is available from the Antlr download site at +https://www.antlr.org/download.html. + +

The Antlr license is available at https://www.antlr.org/license.html. +The license is also reproduced here:

+ +
Copyright (c) 2003-2007, Terence Parr
+All rights reserved.
+Redistribution and use in source and binary forms, with or without modification, 
+are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions 
+and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions 
+and the following disclaimer in the documentation and/or other materials provided with the distribution.
+Neither the name of the author nor the names of its contributors may be used to endorse or promote 
+products derived from this software without specific prior written permission.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+

+ + +

ASM v2.2.3 +

Spring IDE includes a binary version of ASM v2.2.3 (https://asm.objectweb.org/). +The source code for ASM is available from the ObjectWeb download site at +https://asm.objectweb.org/download/. + +

The ASM license is available at https://asm.objectweb.org/license.html. +The license is also reproduced here:

+ +
Copyright (c) 2000-2005 INRIA, France Telecom
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holders nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
+

+ + +

backport-util-concurrent 3.1.0 +

Spring IDE includes a binary version of backport-util-concurrent v3.1.0 (https://dcl.mathcs.emory.edu/util/backport-util-concurrent/). +The source code for backport-util-concurrent is available from the download site at +https://dcl.mathcs.emory.edu/util/backport-util-concurrent/dist/. + + +

The backport-util-concurrent license is available at https://creativecommons.org/licenses/publicdomain. +The license is also reproduced here:

+ +
Copyright-Only Dedication (based on United States law) or Public Domain Certification
+
+The person or persons who have associated work with this document (the 
+"Dedicator" or "Certifier") hereby either (a) certifies that, to the 
+best of his knowledge, the work of authorship identified is in the public 
+domain of the country from which the work is published, or (b) hereby 
+dedicates whatever copyright the dedicators holds in the work of authorship 
+identified below (the "Work") to the public domain. A certifier, moreover, 
+dedicates any copyright interest he may have in the associated work, and 
+for these purposes, is described as a "dedicator" below.
+
+A certifier has taken reasonable steps to verify the copyright status of 
+this work. Certifier recognizes that his good faith efforts may not shield 
+him from liability if in fact the work certified is not in the public domain.
+
+Dedicator makes this dedication for the benefit of the public at large and 
+to the detriment of the Dedicator's heirs and successors. Dedicator intends 
+this dedication to be an overt act of relinquishment in perpetuity of all 
+present and future rights under copyright law, whether vested or contingent, 
+in the Work. Dedicator understands that such relinquishment of all rights 
+includes the relinquishment of all rights to enforce (by lawsuit or otherwise) 
+those copyrights in the Work.
+
+Dedicator recognizes that, once placed in the public domain, the Work may be 
+freely reproduced, distributed, transmitted, used, modified, built upon, or 
+otherwise exploited by anyone for any purpose, commercial or non-commercial, 
+and in any way, including by methods that have not yet been invented or 
+conceived.
+

+ +

AOP Alliance +

LICENCE: all the source code provided by AOP Alliance is Public Domain. +

+ +

javax.el v1.0.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.jms v1.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.servlet v2.5.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.servlet.jsp v2.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.servlet.jsp.jstl v1.2.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.activation v1.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.annotation v1.0.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.mail v1.4.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.xml.stream v1.0.1 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

javax.xml.ws v2.1.0 +

This product includes software distributed by Sun (meanwhile Oracle) under CDDL license +(https://www.sun.com/cddl/cddl.html).

+ +

+ + +
Copyright (c) 2005, 2011 Spring IDE Developers
+ + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/build.properties b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/build.properties new file mode 100644 index 000000000..bdcc25a28 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/build.properties @@ -0,0 +1,6 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + about.html,\ + plugin.xml diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/plugin.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/plugin.xml new file mode 100644 index 000000000..f1dea13e0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/plugin.xml @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/pom.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/pom.xml new file mode 100644 index 000000000..2eaf23663 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + org.springframework.boot.ide + org.springframework.boot.ide + 4.8.1-SNAPSHOT + ../../eclipse-distribution/pom.xml + + + org.springframework.ide.eclipse + org.springframework.ide.eclipse.boot.launch + eclipse-plugin + + + + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + p2 + ignore + + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + second-generate-p2-metadata + + p2-metadata + + verify + + + + + + + + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/AbstractBootLaunchConfigurationDelegate.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/AbstractBootLaunchConfigurationDelegate.java new file mode 100644 index 000000000..aaf3d797d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/AbstractBootLaunchConfigurationDelegate.java @@ -0,0 +1,489 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.launch; + +import static org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Properties; +import java.util.function.Consumer; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jdt.launching.sourcelookup.advanced.AdvancedJavaLaunchDelegate; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.core.BootPreferences; +import org.springframework.ide.eclipse.boot.core.SpringBootCore; +import org.springframework.ide.eclipse.boot.util.ProcessListenerAdapter; +import org.springframework.ide.eclipse.boot.util.ProcessTracker; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public abstract class AbstractBootLaunchConfigurationDelegate extends AdvancedJavaLaunchDelegate { + + private static final String JDT_JAVA_APPLICATION = "org.eclipse.jdt.launching.localJavaApplication"; + + private static final String SILENT_EXIT_EXCEPTION = "org.springframework.boot.devtools.restart.SilentExitExceptionHandler$SilentExitException"; + + private static final String M2E_CLASSPATH_PROVIDER = "org.eclipse.m2e.launchconfig.classpathProvider"; + + protected static final String M2E_SOURCEPATH_PROVIDER = "org.eclipse.m2e.launchconfig.sourcepathProvider"; + public static final String JAVA_LAUNCH_CONFIG_TYPE_ID = IJavaLaunchConfigurationConstants.ID_JAVA_APPLICATION; + public static final String ENABLE_DEBUG_OUTPUT = "spring.boot.debug.enable"; + public static final boolean DEFAULT_ENABLE_DEBUG_OUTPUT = false; + + private static final String BOOT_MAVEN_SOURCE_PATH_PROVIDER = "org.springframework.ide.eclipse.boot.launch.BootMavenSourcePathProvider"; + private static final String BOOT_MAVEN_CLASS_PATH_PROVIDER = "org.springframework.ide.eclipse.boot.launch.BootMavenClassPathProvider"; + private static final String BUILDSHIP_CLASS_PATH_PROVIDER = "org.eclipse.buildship.core.classpathprovider"; + + /** + * Spring boot properties are stored as launch confiuration properties with + * an extra prefix added to property name to avoid name clashes with + * other launch config properties. + */ + private static final String PROPS_PREFIX = "spring.boot.prop."; + + private static final String APPLICATION_PROPERTIES = "spring.boot.app.properties"; + + /** + * To be able to store multiple assignment to the same spring boot + * property name we add a 'oid' at the end of each stored + * property name. ?_SEPERATOR is used to separate the 'real' + * property name from the 'oid' string. + */ + private static final char OID_SEPERATOR = ':'; + + /* + * Remove once old launch properties format is irrelevant + */ + @Deprecated + public static class PropVal { + public String name; + public String value; + public boolean isChecked; + + public PropVal(String name, String value, boolean isChecked) { + //Don't use null, use empty Strings + Assert.isNotNull(name); + Assert.isNotNull(value); + this.name = name; + this.value = value; + this.isChecked = isChecked; + } + + @Override + public String toString() { + return (isChecked?"[X] ":"[ ] ") + + name + "="+ value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (isChecked ? 1231 : 1237); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PropVal other = (PropVal) obj; + if (isChecked != other.isChecked) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } + } + + public static List getLaunchConfigs(IProject p, String confTypeId) { + try { + ILaunchManager lm = getLaunchMan(); + ILaunchConfigurationType type = lm.getLaunchConfigurationType(confTypeId); + if (type!=null) { + ILaunchConfiguration[] configs = lm.getLaunchConfigurations(type); + if (configs!=null && configs.length>0) { + ArrayList result = new ArrayList<>(); + for (ILaunchConfiguration conf : configs) { + if (p.equals(getProject(conf))) { + result.add(conf); + } + } + return result; + } + } + } catch (Exception e) { + Log.log(e); + } + return Collections.emptyList(); + } + + public static void clearProperties(ILaunchConfigurationWorkingCopy conf) { + try { + //note: e43 doesn't use generics for conf.getAttributes, hence the + // funky casting below. + conf.removeAttribute(APPLICATION_PROPERTIES); + + // Legacy properties + for (Object _prefixedProp : conf.getAttributes().keySet()) { + String prefixedProp = (String) _prefixedProp; + if (prefixedProp.startsWith(PROPS_PREFIX)) { + conf.removeAttribute(prefixedProp); + } + } + } catch (Exception e) { + Log.log(e); + } + } + + public static String getMainType(ILaunchConfiguration config) throws CoreException { + return config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, (String)null); + } + + public static void setMainType(ILaunchConfigurationWorkingCopy config, String typeName) { + config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, typeName); + } + + @SuppressWarnings("unchecked") + private static List getProperties(ILaunchConfiguration conf) { + ArrayList props = new ArrayList<>(); + try { + //Note: in e43 conf.getAttributes doesn't use generics yet. So to + //build with 4.3 we need to to some funky casting below. + for (Object _e : conf.getAttributes().entrySet()) { + try { + Map.Entry e = (Entry) _e; + String prefixed = e.getKey(); + if (prefixed.startsWith(PROPS_PREFIX)) { + String name = prefixed.substring(PROPS_PREFIX.length()); + int dotPos = name.lastIndexOf(OID_SEPERATOR); + if (dotPos>=0) { + name = name.substring(0, dotPos); + } + String valueEnablement = (String)e.getValue(); + String value = valueEnablement.substring(1); + boolean enabled = valueEnablement.charAt(0)=='1'; + props.add(new PropVal(name, value, enabled)); + } + } catch (Exception ignore) { + //silently ignore invalid property data. + } + } + } catch (Exception e) { + Log.log(e); + } + return props; + } + + public static String getRawApplicationProperties(ILaunchConfiguration conf) { + try { + if (conf.hasAttribute(APPLICATION_PROPERTIES)) { + return conf.getAttribute(APPLICATION_PROPERTIES, ""); + } else { + // Legacy properties + Properties properties = new Properties(); + getProperties(conf).forEach(p -> properties.setProperty(p.name, p.value)); + StringWriter writer = new StringWriter(); + properties.store(writer, null); + // Remove all comments generated by java.util.Properties serialization + return writer.getBuffer().toString().replaceAll("(?m)^\\s*#.*$", "").trim(); + } + } catch (Exception e) { + Log.log(e); + return ""; + } + } + + public static void setRawApplicationProperties(ILaunchConfigurationWorkingCopy conf, String props) { + conf.setAttribute(APPLICATION_PROPERTIES, props); + } + + public static Properties getApplicationProperties(ILaunchConfiguration conf) { + String propetiesString = getRawApplicationProperties(conf); + Properties properties = new Properties(); + try { + properties.load(new ByteArrayInputStream(propetiesString.getBytes())); + } catch (IOException e) { + Log.log(e); + } + return properties; + } + + @SuppressWarnings("rawtypes") + protected void addPropertiesArguments(ArrayList args, Properties props) { + for (Map.Entry e : props.entrySet()) { + String name = (String) e.getKey(); + String value = (String) e.getValue(); + //spring boot doesn't like empty option keys/values so skip those. + if (!name.isEmpty()) { + args.add(propertyAssignmentArgument(name, value)); + } + } + } + + protected String propertyAssignmentArgument(String name, String value) { + if (name.contains("=")) { + //spring boot has no handling of escape sequences like '\=' + //so we cannot represent keys containing '='. + throw new IllegalArgumentException("property name shouldn't contain '=':"+name); + } + if (value.isEmpty()) { + return "--"+name; + } else { + return "--"+name + "=" +value; + } + } + + + public static boolean getEnableDebugOutput(ILaunchConfiguration conf) { + try { + return conf.getAttribute(ENABLE_DEBUG_OUTPUT, DEFAULT_ENABLE_DEBUG_OUTPUT); + } catch (Exception e) { + Log.log(e); + return DEFAULT_ENABLE_DEBUG_OUTPUT; + } + } + + public static void setEnableDebugOutput(ILaunchConfigurationWorkingCopy conf, boolean enable) { + conf.setAttribute(ENABLE_DEBUG_OUTPUT, enable); + } + + /** + * Get the project associated with this a luanch config. Note that this + * method returns an IProject reference regardless of whether or not the + * project exists. + */ + public static IProject getProject(ILaunchConfiguration conf) { + try { + String pname = getProjectName(conf); + if (StringUtil.hasText(pname)) { + IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(pname); + //debug(conf, "getProject => "+p); + return p; + } + } catch (Exception e) { + Log.log(e); + } + //debug(conf, "getProject => NULL"); + return null; + } + + public static String getProjectName(ILaunchConfiguration conf) + throws CoreException { + return conf.getAttribute(ATTR_PROJECT_NAME, ""); + } + + public static void setProject(ILaunchConfigurationWorkingCopy conf, IProject p) { + //debug(conf, "setProject <= "+p); + if (p==null) { + conf.removeAttribute(ATTR_PROJECT_NAME); + } else { + conf.setAttribute(ATTR_PROJECT_NAME, p.getName()); + } + } + + /** + * Enable maven classpath provider if applicable to this conf. + * Addresses https://issuetracker.springsource.com/browse/STS-4085 + */ + static void enableMavenClasspathProvider(ILaunchConfigurationWorkingCopy conf) { + try { + if (conf.getType().getIdentifier().equals(JAVA_LAUNCH_CONFIG_TYPE_ID)) { + //Take care not to add this a 'real' Boot launch config or it will cause m2e to throw exceptions + //These 'magic' attributes should only be added to a 'cloned' copy of our config with the right type. + IProject p = getProject(conf); + if (p!=null && p.hasNature(SpringBootCore.M2E_NATURE)) { + if (!conf.hasAttribute(IJavaLaunchConfigurationConstants.ATTR_CLASSPATH_PROVIDER)) { + conf.setAttribute(IJavaLaunchConfigurationConstants.ATTR_CLASSPATH_PROVIDER, M2E_CLASSPATH_PROVIDER); + } + if (!conf.hasAttribute(IJavaLaunchConfigurationConstants.ATTR_SOURCE_PATH_PROVIDER)) { + conf.setAttribute(IJavaLaunchConfigurationConstants.ATTR_SOURCE_PATH_PROVIDER, M2E_SOURCEPATH_PROVIDER); + } + } + } + } catch (Exception e) { + BootActivator.log(e); + } + } + + public static ILaunchManager getLaunchMan() { + return DebugPlugin.getDefault().getLaunchManager(); + } + + @Override + public void launch(ILaunchConfiguration conf, String mode, final ILaunch launch, IProgressMonitor monitor) + throws CoreException { + conf = configureClassPathProviders(conf); + if (ILaunchManager.DEBUG_MODE.equals(mode) && isIgnoreSilentExitException(conf)) { + final IgnoreExceptionOfType breakpointListener = new IgnoreExceptionOfType(launch, SILENT_EXIT_EXCEPTION); + new ProcessTracker(new ProcessListenerAdapter() { + @Override + public void debugTargetTerminated(ProcessTracker tracker, IDebugTarget target) { + if (launch.equals(target.getLaunch())){ + breakpointListener.dispose(); + tracker.dispose(); + } + } + }); + } + super.launch(conf, mode, launch, monitor); + } + + protected ILaunchConfiguration configureClassPathProviders(ILaunchConfiguration conf) throws CoreException { + IProject project = BootLaunchConfigurationDelegate.getProject(conf); + if (project!=null) { + if (project.hasNature(SpringBootCore.M2E_NATURE)) { + conf = modify(conf, (ILaunchConfigurationWorkingCopy wc) -> { + enableMavenClasspathProviders(wc); + }); + } else if (project.hasNature(SpringBootCore.BUILDSHIP_NATURE)) { + conf = modify(conf, wc -> { + enableGradleClasspathProviders(wc); + }); + } + } + return conf; + } + + @Override + public String[][] getClasspathAndModulepath(ILaunchConfiguration configuration) throws CoreException { + if (configuration.hasAttribute(IJavaLaunchConfigurationConstants.ATTR_EXCLUDE_TEST_CODE)) { + //TODO: This is a dirty hack. We 'trick' BuildShip to treat our launch config as if it is a plain JDT launch by making + // a temporary copy of it. + //A request to make BuildShip provide a cleaner way to do this was filed here: + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=543328 + //If that bug is resolved this code should be removed. + configuration = BootLaunchConfigurationDelegate.copyAs(configuration, JDT_JAVA_APPLICATION); + } + return super.getClasspathAndModulepath(configuration); + } + + @Override + public String[] getClasspath(ILaunchConfiguration configuration) throws CoreException { + if (configuration.hasAttribute(IJavaLaunchConfigurationConstants.ATTR_EXCLUDE_TEST_CODE)) { + //TODO: This is a dirty hack. We 'trick' BuildShip to treat our launch config as if it is a plain JDT launch by making + // a temporary copy of it. + //A request to make BuildShip provide a cleaner way to do this was filed here: + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=543328 + //If that bug is resolved this code should be removed. + configuration = BootLaunchConfigurationDelegate.copyAs(configuration, JDT_JAVA_APPLICATION); + } + return super.getClasspath(configuration); + } + + @Override + public String[][] getBootpathExt(ILaunchConfiguration configuration) throws CoreException { + if (configuration.hasAttribute(IJavaLaunchConfigurationConstants.ATTR_EXCLUDE_TEST_CODE)) { + //TODO: This is a dirty hack. We 'trick' BuildShip to treat our launch config as if it is a plain JDT launch by making + // a temporary copy of it. + //A request to make BuildShip provide a cleaner way to do this was filed here: + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=543328 + //If that bug is resolved this code should be removed. + configuration = BootLaunchConfigurationDelegate.copyAs(configuration, JDT_JAVA_APPLICATION); + } + return super.getBootpathExt(configuration); + } + + public static void enableMavenClasspathProviders(ILaunchConfigurationWorkingCopy wc) { + setAttribute(wc, IJavaLaunchConfigurationConstants.ATTR_SOURCE_PATH_PROVIDER, BOOT_MAVEN_SOURCE_PATH_PROVIDER); + setAttribute(wc, IJavaLaunchConfigurationConstants.ATTR_CLASSPATH_PROVIDER, BOOT_MAVEN_CLASS_PATH_PROVIDER); + try { + if (!wc.hasAttribute(IJavaLaunchConfigurationConstants.ATTR_EXCLUDE_TEST_CODE)) { + setAttribute(wc, IJavaLaunchConfigurationConstants.ATTR_EXCLUDE_TEST_CODE, true); + } + } catch (CoreException e) { + Log.log(e); + } + } + + public static void enableGradleClasspathProviders(ILaunchConfigurationWorkingCopy wc) { + /* This is found in typical java launch config for buildship project. It plays a crucial role in + * computing correct runtime classpath: + * + * + * + */ + setAttribute(wc, IJavaLaunchConfigurationConstants.ATTR_CLASSPATH_PROVIDER, BUILDSHIP_CLASS_PATH_PROVIDER); + } + + private ILaunchConfiguration modify(ILaunchConfiguration conf, Consumer mutator) throws CoreException { + ILaunchConfigurationWorkingCopy wc = conf.getWorkingCopy(); + try { + mutator.accept(wc); + } finally { + if (wc.isDirty()) { + conf = wc.doSave(); + } + } + return conf; + } + + private static void setAttribute(ILaunchConfigurationWorkingCopy wc, String a, boolean v) { + try { + if (!wc.hasAttribute(a) || v != wc.getAttribute(a, false)) { + wc.setAttribute(a, v); + } + } catch (CoreException e) { + Log.log(e); + } + } + + private static void setAttribute(ILaunchConfigurationWorkingCopy wc, String a, String v) { + try { + if (!Objects.equals(v, wc.getAttribute(a, (String)null))) { + wc.setAttribute(a, v); + } + } catch (CoreException e) { + Log.log(e); + } + } + + public static boolean isIgnoreSilentExitException(ILaunchConfiguration conf) { + //This might be controlled by individual launch conf in future, but for now, it is just a global preference. + return BootPreferences.getInstance().isIgnoreSilentExit(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootClasspathTab.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootClasspathTab.java new file mode 100644 index 000000000..2260c089f --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootClasspathTab.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2018 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.launch; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.ui.ILaunchConfigurationTab; +import org.eclipse.jdt.debug.ui.launchConfigurations.JavaClasspathTab; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.springframework.ide.eclipse.boot.core.SpringBootCore; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +public class BootClasspathTab extends JavaClasspathTab implements ILaunchConfigurationTab { + + @Override + public void initializeFrom(ILaunchConfiguration conf) { + //See bug: https://github.com/spring-projects/spring-ide/issues/222 + // If user did not create boot launch config via boot dash and tries to immediately edit classpath + // an incorrect default classpath will be computed because of the missing classpath provider. + // So make sure the classpath provider is present. + IProject project = BootLaunchConfigurationDelegate.getProject(conf); + try { + if (project!=null && project.hasNature(SpringBootCore.M2E_NATURE) && !conf.hasAttribute(IJavaLaunchConfigurationConstants.ATTR_CLASSPATH_PROVIDER)) { + ILaunchConfigurationWorkingCopy wc = conf.getWorkingCopy(); + BootLaunchConfigurationDelegate.enableMavenClasspathProviders(wc); + conf = wc; + } + } catch (CoreException e) { + Log.log(e); + } + super.initializeFrom(conf); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java new file mode 100644 index 000000000..f17c71a7b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch; + +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.preferences.DefaultScope; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.service.prefs.BackingStoreException; +import org.springframework.ide.eclipse.boot.launch.util.BootLaunchConfDeleter; + +/** + * @author Kris De Volder + */ +public class BootLaunchActivator extends AbstractUIPlugin { + + private static BootLaunchActivator instance; + + private BootLaunchConfDeleter workspaceListener; + + public BootLaunchActivator() { + } + + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + workspaceListener = new BootLaunchConfDeleter(ResourcesPlugin.getWorkspace(), DebugPlugin.getDefault().getLaunchManager()); + instance = this; + + IPreferenceStore myStore = instance.getPreferenceStore(); + if (!myStore.getBoolean("cglib.breakpoint.warning.disabled")) { + setPreference("org.eclipse.jdt.debug.ui", "org.eclipse.jdt.debug.ui.prompt_unable_to_install_breakpoint", false); + myStore.setValue("cglib.breakpoint.warning.disabled", true); + } + } + + private void setPreference(String plugin, String key, boolean value) { + try { + IEclipsePreferences store = DefaultScope.INSTANCE.getNode(plugin); + store.putBoolean(key, value); + store.flush(); + } catch (BackingStoreException e) { + //ignore + } + try { + IEclipsePreferences store = InstanceScope.INSTANCE.getNode(plugin); + store.putBoolean(key, value); + store.flush(); + } catch (BackingStoreException e) { + //ignore + } + } + + @Override + public void stop(BundleContext context) throws Exception { + instance = null; + if (workspaceListener!=null) { + workspaceListener.dispose(); + } + super.stop(context); + } + + public static BootLaunchActivator getInstance() { + return instance; + } + + private static final String LIVEBEAN_SUPPORT_BUNDLE_ID = "org.springframework.ide.eclipse.boot.launch.livebean"; + public boolean isLiveBeanSupported() { + for (Bundle bndl : getBundle().getBundleContext().getBundles()) { + if (bndl.getSymbolicName().equals(LIVEBEAN_SUPPORT_BUNDLE_ID)) { + return true; + } + } + return false; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchConfigurationDelegate.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchConfigurationDelegate.java new file mode 100644 index 000000000..c7c0ed9b0 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchConfigurationDelegate.java @@ -0,0 +1,652 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch; + +import static org.eclipse.debug.core.DebugPlugin.ATTR_PROCESS_FACTORY_ID; +import static org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS; +import static org.springsource.ide.eclipse.commons.core.util.StringUtil.hasText; + +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Properties; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Platform; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jdt.launching.IRuntimeClasspathEntry; +import org.eclipse.jdt.launching.JavaRuntime; +import org.osgi.framework.Bundle; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.core.BootPreferences; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springframework.ide.eclipse.boot.core.SpringBootCore; +import org.springframework.ide.eclipse.boot.launch.livebean.JmxBeanSupport; +import org.springframework.ide.eclipse.boot.launch.livebean.JmxBeanSupport.Feature; +import org.springframework.ide.eclipse.boot.launch.process.BootProcessFactory; +import org.springframework.ide.eclipse.boot.launch.profiles.ProfileHistory; +import org.springframework.ide.eclipse.boot.launch.util.PortFinder; +import org.springsource.ide.eclipse.commons.core.util.OsUtils; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * @author Kris De Volder + */ +public class BootLaunchConfigurationDelegate extends AbstractBootLaunchConfigurationDelegate { + + private static DeletedLaunchConfTerminator deletedLaunchConfTerminator = null; + + public synchronized static void ensureDeletedLaunchConfTerminator() { + if (deletedLaunchConfTerminator==null) { + deletedLaunchConfTerminator = new DeletedLaunchConfTerminator(DebugPlugin.getDefault().getLaunchManager(), (ILaunch l) -> { + try { + return l!=null && Boolean.valueOf(l.getAttribute(BOOT_LAUNCH_MARKER)); + } catch (Exception e) { + Log.log(e); + return false; + } + }); + } + } + + + +// private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder"); +// private static void debug(String string) { +// if (DEBUG) { +// System.out.println(string); +// } +// } + + public static final String TYPE_ID = "org.springframework.ide.eclipse.boot.launch"; + + /** + * Launch attribute that helps recognize a launch as a boot launch even after the launch configuration has + * been deleted. + */ + public static final String BOOT_LAUNCH_MARKER = "isBootLaunch"; + + public static final String ENABLE_LIVE_BEAN_SUPPORT = "spring.boot.livebean.enable"; + public static final boolean DEFAULT_ENABLE_LIVE_BEAN_SUPPORT() { + BootLaunchActivator ins = BootLaunchActivator.getInstance(); + return ins!=null && ins.isLiveBeanSupported(); + } + + public static final String ENABLE_JMX = "spring.boot.jmx.enable"; + public static final boolean DEFAULT_ENABLE_JMX = true; + + public static final String JMX_PORT = "spring.boot.livebean.port"; + + public static final int DEFAULT_JMX_PORT = 0; //means pick it dynamically + + public static final String ANSI_CONSOLE_OUTPUT = "spring.boot.ansi.console"; + + public static final String FAST_STARTUP = "spring.boot.fast.startup"; + + private static final String PROFILE = "spring.boot.profile"; + public static final String DEFAULT_PROFILE = ""; + + public static final String ENABLE_LIFE_CYCLE = "spring.boot.lifecycle.enable"; + public static final boolean DEFAULT_ENABLE_LIFE_CYCLE = true; + + public static final String HIDE_FROM_BOOT_DASH = "spring.boot.dash.hidden"; + public static final boolean DEFAULT_HIDE_FROM_BOOT_DASH = false; + + public static final String PROCESS_ID = "spring.boot.process.id"; + + private static final String ENABLE_CHEAP_ENTROPY_VM_ARGS = "-Djava.security.egd=file:/dev/./urandom "; + private static final String TERMINATION_TIMEOUT = "spring.boot.lifecycle.termination.timeout"; + public static final long DEFAULT_TERMINATION_TIMEOUT = 15000; // 15 seconds + + public static final String USE_THIN_WRAPPER = "spring.boot.thinwrapper.enable"; + public static final boolean DEFAULT_USE_THIN_WRAPPER = false; + + public static final String SPRING_PROJECT_NAME_ATTRIBUTE = "spring.boot.project.name"; + + private ProfileHistory profileHistory = new ProfileHistory(); + + /** + * Use threadlocal to gain access to current launch in some of the methods + * (i.e. getVMArguments in particular) of the {@link AbstractBootLaunchConfigurationDelegate} + * framework that, unfortunately don't pass it along as parameters. It's either this, or copy + * a whole bunch of inherited code just so we can modify it to add an extra argument. + */ + private static final ThreadLocal CURRENT_LAUNCH = new ThreadLocal<>(); + + @Override + public void launch(ILaunchConfiguration conf, String mode, + ILaunch launch, IProgressMonitor monitor) throws CoreException { + ensureDeletedLaunchConfTerminator(); + launch.setAttribute(BOOT_LAUNCH_MARKER, "true"); + CURRENT_LAUNCH.set(launch); + try { + profileHistory.updateHistory(getProject(conf), getProfile(conf)); + super.launch(conf, mode, launch, monitor); + } finally { + CURRENT_LAUNCH.remove(); + } + } + + @Override + public String getProgramArguments(ILaunchConfiguration conf) throws CoreException { + Properties props = getApplicationProperties(conf); + String profile = getProfile(conf); + boolean debugOutput = getEnableDebugOutput(conf); + boolean enableAnsiConsole = supportsAnsiConsoleOutput() && getEnableAnsiConsoleOutput(conf); + if ((props==null || props.isEmpty()) && !debugOutput && !hasText(profile) && !enableAnsiConsole && !useThinWrapper(conf)) { + //shortcut for case where no boot-specific customizations are specified. + return super.getProgramArguments(conf); + } + ArrayList args = new ArrayList<>(); + if (useThinWrapper(conf)) { + String realMain = super.getMainTypeName(conf); + //--thin.main=com.example.SampleApplication + // --thin.archive=target/classes - the first one is the main class of the boot app, as already configured in the boot launch config, the second one points to the compiled classes (the entry that was on the regular classpath before without the maven dependencies) + args.add("--thin.main="+realMain); + args.add("--thin.archive="+getThinArchive(conf)); + } + if (debugOutput) { + args.add("--debug"); + } + if (hasText(profile)) { + args.add(propertyAssignmentArgument("spring.profiles.active", profile)); + } + if (enableAnsiConsole) { + args.add(propertyAssignmentArgument("spring.output.ansi.enabled", "always")); + } + addPropertiesArguments(args, props); + args.addAll(Arrays.asList(DebugPlugin.parseArguments(super.getProgramArguments(conf)))); + return DebugPlugin.renderArguments(args.toArray(new String[args.size()]), null); + } + + private String getThinArchive(ILaunchConfiguration conf) throws CoreException { + IRuntimeClasspathEntry[] entries = { + JavaRuntime.newProjectRuntimeClasspathEntry(getJavaProject(conf)) + }; + IRuntimeClasspathEntry[] realClasspath = JavaRuntime.resolveRuntimeClasspath(entries, conf); + StringBuilder classpathString = new StringBuilder(); + boolean first = true; + for (IRuntimeClasspathEntry entry : realClasspath) { + if (!first) { + classpathString.append(File.pathSeparatorChar); + } + classpathString.append(entry.getLocation()); + first = false; + } + if (first) { + throw new IllegalStateException("Could not determine the 'thin archive' location"); + } + return classpathString.toString(); + } + + @Override + public String getVMArguments(ILaunchConfiguration conf) + throws CoreException { + try { + List vmArgs = new ArrayList<>(); + vmArgs.addAll(Arrays.asList(DebugPlugin.parseArguments(super.getVMArguments(conf)))); + // VM args for JMX connection + EnumSet enabled = getEnabledJmxFeatures(conf); + if (!enabled.isEmpty()) { + int port = 0; + try { + port = Integer.parseInt(getJMXPort(conf)); + } catch (Exception e) { + //ignore: bad data in launch config. + } + if (port==0) { + port = PortFinder.findFreePort(); //slightly better than calling JmxBeanSupport.randomPort() + } + String[] enableLiveBeanArgs = DebugPlugin.parseArguments(JmxBeanSupport.jmxBeanVmArgs(port, enabled)); + for (int i = 0; i < enableLiveBeanArgs.length; i++) { + vmArgs.add(i, enableLiveBeanArgs[i]); + } + ILaunch currentLaunch = CURRENT_LAUNCH.get(); + if (currentLaunch!=null) { + currentLaunch.setAttribute(JMX_PORT, ""+port); + } + } + // Fast startup VM args + String fastStartupArgs = BootActivator.getDefault().getPreferenceStore().getString(BootPreferences.PREF_BOOT_FAST_STARTUP_JVM_ARGS); + boolean fastStartup = getFastStartup(conf) && fastStartupArgs != null && !fastStartupArgs.isEmpty(); + if (fastStartup && !fastStartupArgs.trim().isEmpty()) { + // Add space to separate fast startup args from the preceding arguments + vmArgs.addAll(Arrays.asList(DebugPlugin.parseArguments(fastStartupArgs))); + } + + String projectName = AbstractBootLaunchConfigurationDelegate.getProjectName(conf); + vmArgs.add("-D" + SPRING_PROJECT_NAME_ATTRIBUTE + "=" + projectName); + + return DebugPlugin.renderArguments(vmArgs.toArray(new String[vmArgs.size()]), null); + } catch (Exception e) { + Log.log(e); + } + return super.getVMArguments(conf); + } + + public static EnumSet getEnabledJmxFeatures(ILaunchConfiguration conf) { + EnumSet enabled = EnumSet.noneOf(Feature.class); + if (getEnableJmx(conf)) { + enabled.add(Feature.JMX); + } + if (getEnableLiveBeanSupport(conf)) { + enabled.add(Feature.LIVE_BEAN_GRAPH); + } + if (getEnableLifeCycle(conf)) { + enabled.add(Feature.LIFE_CYCLE); + } + return enabled; + } + + public static boolean isHiddenFromBootDash(ILaunchConfiguration conf) { + try { + return conf.getAttribute(HIDE_FROM_BOOT_DASH, DEFAULT_HIDE_FROM_BOOT_DASH); + } catch (CoreException e) { + Log.log(e); + } + return DEFAULT_HIDE_FROM_BOOT_DASH; + } + + public static void setHiddenFromBootDash(ILaunchConfigurationWorkingCopy conf, boolean hide) { + conf.setAttribute(HIDE_FROM_BOOT_DASH, hide); + } + + /** + * Retrieve the 'Enable Life Cycle Tracking' option from the config. Note that + * this doesn't necesarily mean that this feature is effectively enabled as + * it is only supported on recent enough versions of Boot. + *

+ * See also the 'supportsLifeCycleManagement' method. + */ + public static boolean getEnableLifeCycle(ILaunchConfiguration conf) { + try { + return conf.getAttribute(ENABLE_LIFE_CYCLE, DEFAULT_ENABLE_LIFE_CYCLE); + } catch (Exception e) { + Log.log(e); + } + return DEFAULT_ENABLE_LIFE_CYCLE; + } + + public static boolean getFastStartup(ILaunchConfiguration conf) { + boolean defaultValue = BootActivator.getDefault().getPreferenceStore().getBoolean(BootPreferences.PREF_BOOT_FAST_STARTUP_DEFAULT); + try { + return conf.getAttribute(FAST_STARTUP, defaultValue); + } catch (Exception e) { + Log.log(e); + } + return defaultValue; + } + + public static void setEnableJMX(ILaunchConfigurationWorkingCopy wc, boolean enable) { + wc.setAttribute(ENABLE_JMX, enable); + } + + public static void setEnableLifeCycle(ILaunchConfigurationWorkingCopy wc, boolean enable) { + wc.setAttribute(ENABLE_LIFE_CYCLE, enable); + } + + public static void setFastStartup(ILaunchConfigurationWorkingCopy wc, boolean enable) { + wc.setAttribute(FAST_STARTUP, enable); + } + + public static boolean canUseLifeCycle(ILaunchConfiguration conf) { + return BootLaunchConfigurationDelegate.getEnableLifeCycle(conf) + && BootLaunchConfigurationDelegate.supportsLifeCycleManagement(conf); + } + + public static boolean canUseLifeCycle(ILaunch launch) { + ILaunchConfiguration conf = launch.getLaunchConfiguration(); + return conf!=null && canUseLifeCycle(conf); + } + + public static boolean supportsLifeCycleManagement(ILaunchConfiguration conf) { + IProject p = getProject(conf); + if (p!=null) { + return BootPropertyTester.supportsLifeCycleManagement(p); + } + return false; + } + + /** + * Sets minimal default values to create a runnable launch configuration. + */ + public static void setDefaults(ILaunchConfigurationWorkingCopy wc, + IProject project, + String mainType + ) throws CoreException { + setProcessFactory(wc, BootProcessFactory.class); + setProject(wc, project); + if (project!=null && project.hasNature(SpringBootCore.M2E_NATURE)) { + enableMavenClasspathProviders(wc); + } else if (project!=null && project.hasNature(SpringBootCore.BUILDSHIP_NATURE)) { + enableGradleClasspathProviders(wc); + } + wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_EXCLUDE_TEST_CODE, true); + if (mainType!=null) { + setMainType(wc, mainType); + } + setEnableJMX(wc, DEFAULT_ENABLE_JMX); + setEnableLiveBeanSupport(wc, DEFAULT_ENABLE_LIVE_BEAN_SUPPORT()); + setEnableLifeCycle(wc, DEFAULT_ENABLE_LIFE_CYCLE); + setTerminationTimeout(wc,""+DEFAULT_TERMINATION_TIMEOUT); + setJMXPort(wc, ""+DEFAULT_JMX_PORT); + if (!OsUtils.isWindows()) { + setVMArgs(wc, ENABLE_CHEAP_ENTROPY_VM_ARGS); + } + } + + public static void setTerminationTimeout(ILaunchConfigurationWorkingCopy wc, String value) { + wc.setAttribute(TERMINATION_TIMEOUT, ""+value); + } + + public static String getTerminationTimeout(ILaunchConfiguration conf) { + try { + return conf.getAttribute(TERMINATION_TIMEOUT, ""+DEFAULT_TERMINATION_TIMEOUT); + } catch (Exception e) { + Log.log(e); + return ""+DEFAULT_TERMINATION_TIMEOUT; + } + } + + public static long getTerminationTimeoutAsLong(ILaunchConfiguration conf) { + String v = getTerminationTimeout(conf); + if (StringUtil.hasText(v)) { + try { + return Long.parseLong(v); + } catch (Exception e) { + Log.log(e); + } + } + return DEFAULT_TERMINATION_TIMEOUT; + } + + + private static void setVMArgs(ILaunchConfigurationWorkingCopy wc, String vmArgs) { + wc.setAttribute(ATTR_VM_ARGUMENTS, vmArgs); + } + + /** + * Notes: + *

+ * 1. we are assuming that the processFactoryId is the same as the classname of + * the class that implements it. This is not a given, but a convenient and logical convention. + *

+ * 2. The class must be registered to this ID using plugin.xml (extension point + * org.eclipse.debug.core.processFactories) + */ + public static void setProcessFactory(ILaunchConfigurationWorkingCopy wc, Class klass) { + wc.setAttribute(ATTR_PROCESS_FACTORY_ID, klass.getName()); + } + + public static boolean getEnableJmx(ILaunchConfiguration conf) { + try { + return conf.getAttribute(ENABLE_JMX, DEFAULT_ENABLE_JMX); + } catch (Exception e) { + Log.log(e); + } + return DEFAULT_ENABLE_JMX; + } + + public static boolean getEnableLiveBeanSupport(ILaunchConfiguration conf) { + try { + return conf.getAttribute(ENABLE_LIVE_BEAN_SUPPORT, DEFAULT_ENABLE_LIVE_BEAN_SUPPORT()); + } catch (Exception e) { + Log.log(e); + } + return DEFAULT_ENABLE_LIVE_BEAN_SUPPORT(); + } + + public static String getJMXPort(ILaunchConfiguration conf) { + try { + return conf.getAttribute(JMX_PORT, ""); + } catch (CoreException e) { + Log.log(e); + } + return ""; + } + + public static void setEnableLiveBeanSupport(ILaunchConfigurationWorkingCopy conf, boolean value) { + conf.setAttribute(ENABLE_LIVE_BEAN_SUPPORT, value); + } + + public static void setJMXPort(ILaunchConfigurationWorkingCopy conf, String portAsStr) { + conf.setAttribute(JMX_PORT, portAsStr); + } + + public static String getProfile(ILaunchConfiguration conf) { + try { + return conf.getAttribute(PROFILE, DEFAULT_PROFILE); + } catch (CoreException e) { + Log.log(e); + return DEFAULT_PROFILE; + } + } + + public static void setProfile(ILaunchConfigurationWorkingCopy conf, String profile) { + conf.setAttribute(PROFILE, profile); + } + + public static ILaunchConfiguration duplicate(ILaunchConfiguration conf) throws CoreException { + String newName = DebugPlugin.getDefault().getLaunchManager().generateLaunchConfigurationName(conf.getName()); + ILaunchConfigurationWorkingCopy copy = conf.copy(newName); + + int existingJmxPort = getJMXPortAsInt(conf); + if (existingJmxPort>0) { + //change port on duplicated config, but only if it was set to a specific port. + setJMXPort(copy, ""+JmxBeanSupport.randomPort()); + } + return copy.doSave(); + } + + + public static ILaunchConfigurationWorkingCopy createWorkingCopy(String nameHint) throws CoreException { + String name = getLaunchMan().generateLaunchConfigurationName(nameHint); + return getConfType().newInstance(null, name); + } + + public static ILaunchConfigurationType getConfType() { + return getLaunchMan().getLaunchConfigurationType(TYPE_ID); + } + + public static ILaunchConfiguration createConf(IType type) throws CoreException { + ILaunchConfigurationWorkingCopy wc = createWorkingCopy(type); + return wc.doSave(); + } + + public static ILaunchConfigurationWorkingCopy createWorkingCopy(IType type) throws CoreException { + ILaunchConfigurationWorkingCopy wc = null; + ILaunchConfigurationType configType = getConfType(); + IProject project = type.getJavaProject().getProject(); + String projectName = type.getJavaProject().getElementName(); + String shortTypeName = type.getTypeQualifiedName('.'); + String typeName = type.getFullyQualifiedName(); + wc = configType.newInstance(null, getLaunchMan().generateLaunchConfigurationName( + projectName+" - "+shortTypeName)); + BootLaunchConfigurationDelegate.setDefaults(wc, project, typeName); + wc.setMappedResources(new IResource[] {type.getUnderlyingResource()}); + return wc; + } + + public static ILaunchConfiguration createConf(IProject project) throws CoreException { + return createConf(JavaCore.create(project)); + } + + public static ILaunchConfiguration createConf(IJavaProject project) throws CoreException { + ILaunchConfigurationWorkingCopy wc = null; + ILaunchConfigurationType configType = getConfType(); + String projectName = project.getElementName(); + wc = configType.newInstance(null, getLaunchMan().generateLaunchConfigurationName(projectName)); + BootLaunchConfigurationDelegate.setDefaults(wc, project.getProject(), null); + wc.setMappedResources(new IResource[] {project.getUnderlyingResource()}); + return wc.doSave(); + } + + public static int getJMXPortAsInt(ILaunchConfiguration conf) { + String jmxPortStr = getJMXPort(conf); + if (jmxPortStr!=null) { + try { + return Integer.parseInt(jmxPortStr); + } catch (Exception e) { + //Ignore + } + } + return -1; + } + + public static int getJMXPortAsInt(ILaunch launch) { + String jmxPortStr = launch.getAttribute(JMX_PORT); + if (jmxPortStr!=null) { + try { + return Integer.parseInt(jmxPortStr); + } catch (Exception e) { + //Ignore + } + } + return -1; + } + + public static long getTerminationTimeoutAsLong(ILaunch launch) { + ILaunchConfiguration conf = launch.getLaunchConfiguration(); + if (conf!=null) { + return BootLaunchConfigurationDelegate.getTerminationTimeoutAsLong(conf); + } + return BootLaunchConfigurationDelegate.DEFAULT_TERMINATION_TIMEOUT; + } + + public static boolean supportsAnsiConsoleOutput() { + Bundle bundle = Platform.getBundle("net.mihai-nita.ansicon.plugin"); + return bundle != null && bundle.getState() != Bundle.UNINSTALLED; + } + + public static boolean getEnableAnsiConsoleOutput(ILaunchConfiguration conf) { + boolean defaultValue = supportsAnsiConsoleOutput(); + try { + return conf.getAttribute(ANSI_CONSOLE_OUTPUT, defaultValue); + } catch (CoreException e) { + return defaultValue; + } + } + + public static void setEnableAnsiConsoleOutput(ILaunchConfigurationWorkingCopy wc, boolean enable) { + wc.setAttribute(ANSI_CONSOLE_OUTPUT, enable); + } + + @Override + public String[][] getClasspathAndModulepath(ILaunchConfiguration conf) throws CoreException { + //with Java 9 Beta installed we need this method, because getClasspath is no longer called. + try { + if (useThinWrapper(conf)) { + return new String[][] { + getClasspath(conf), + new String[] {} + }; + }; + return super.getClasspathAndModulepath(conf); + } catch (Throwable e) { + throw ExceptionUtil.coreException(e); + } + } + + @Override + public String[] getClasspath(ILaunchConfiguration conf) throws CoreException { + if (useThinWrapper(conf)) { + File thinWrapper = BootPreferences.getInstance().getThinWrapper(); + Assert.isLegal(thinWrapper!=null, "'Use thin wrapper' option is selected, but thin wrapper is not defined"); + Assert.isLegal(thinWrapper.isFile(), "'Use thin wrapper' option is selected, but thin wrapper ("+thinWrapper+") is not an existing file"); + return new String[] { + thinWrapper.getAbsolutePath() + }; + } + return super.getClasspath(conf); + } + + @Override + public String getMainTypeName(ILaunchConfiguration conf) throws CoreException { + try { + if (useThinWrapper(conf)) { + return getThinWrapperMain(conf); + } + } catch (Exception e) { + Log.log(e); + } + return super.getMainTypeName(conf); + } + + protected String getThinWrapperMain(ILaunchConfiguration conf) throws Exception { + File thinWrapper = BootPreferences.getInstance().getThinWrapper(); + try (ZipInputStream zipStream = new ZipInputStream(new FileInputStream(thinWrapper))) { + ZipEntry zipEntry; + while (null!=(zipEntry = zipStream.getNextEntry())) { + String name = zipEntry.getName(); + if (name.equals("META-INF/MANIFEST.MF")) { + Manifest manifest = new Manifest(zipStream); + String mainClass = manifest.getMainAttributes().getValue("Main-Class"); + if (mainClass!=null) { + return mainClass; + } else { + throw new IllegalArgumentException("Thin wrapper '"+thinWrapper+"' doesn't have a 'Main-Class' attribute in its jar manifest"); + } + } + } + throw new IllegalArgumentException("No META-INF/MANIFEST.MF found in '"+thinWrapper+"'. Is it a proper 'thin boot wrapper' jar?"); + } + } + + /** + * Copy a given launch config into a 'clone' that has all the same attributes but + * a different type id. + */ + public static ILaunchConfigurationWorkingCopy copyAs(ILaunchConfiguration conf, + String newType) throws CoreException { + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunchConfigurationType launchConfigurationType = launchManager + .getLaunchConfigurationType(newType); + ILaunchConfigurationWorkingCopy wc = launchConfigurationType.newInstance(null, + launchManager.generateLaunchConfigurationName(conf.getName())); + wc.setAttributes(conf.getAttributes()); + return wc; + } + + public static boolean useThinWrapper(ILaunchConfiguration conf) { + try { + return BootPreferences.getInstance().getThinWrapper()!=null && conf.getAttribute(USE_THIN_WRAPPER, DEFAULT_USE_THIN_WRAPPER); + } catch (CoreException e) { + Log.log(e); + } + return DEFAULT_USE_THIN_WRAPPER; + } + + public static void setUseThinWrapper(ILaunchConfigurationWorkingCopy wc, boolean b) { + wc.setAttribute(USE_THIN_WRAPPER, b); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchConfigurationTabGroup.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchConfigurationTabGroup.java new file mode 100644 index 000000000..aa2784134 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchConfigurationTabGroup.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2015 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch; + + +import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup; +import org.eclipse.debug.ui.CommonTab; +import org.eclipse.debug.ui.EnvironmentTab; +import org.eclipse.debug.ui.ILaunchConfigurationDialog; +import org.eclipse.debug.ui.ILaunchConfigurationTab; +import org.eclipse.debug.ui.ILaunchConfigurationTabGroup; +import org.eclipse.debug.ui.sourcelookup.SourceLookupTab; +import org.eclipse.jdt.debug.ui.launchConfigurations.JavaArgumentsTab; +import org.eclipse.jdt.debug.ui.launchConfigurations.JavaClasspathTab; +import org.eclipse.jdt.debug.ui.launchConfigurations.JavaJRETab; + +public class BootLaunchConfigurationTabGroup extends AbstractLaunchConfigurationTabGroup { + + /** + * @see ILaunchConfigurationTabGroup#createTabs(ILaunchConfigurationDialog, String) + */ + public void createTabs(ILaunchConfigurationDialog dialog, String mode) { + ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] { + new BootMainTab(), + new JavaArgumentsTab(), + new JavaJRETab(), + new BootClasspathTab(), + new SourceLookupTab(), + new EnvironmentTab(), + new CommonTab() + }; + setTabs(tabs); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchShortcut.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchShortcut.java new file mode 100644 index 000000000..29bb9786c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchShortcut.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright (c) 2013 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch; + +import java.lang.reflect.InvocationTargetException; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.debug.ui.launchConfigurations.JavaApplicationLaunchShortcut; +import org.eclipse.jdt.internal.debug.ui.JDIDebugUIPlugin; +import org.eclipse.jdt.internal.debug.ui.launcher.LauncherMessages; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.operation.IRunnableContext; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.viewers.StructuredSelection; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springsource.ide.eclipse.commons.frameworks.core.ExceptionUtil; +import org.springsource.ide.eclipse.commons.frameworks.core.maintype.MainTypeFinder; + +@SuppressWarnings("restriction") +public class BootLaunchShortcut extends JavaApplicationLaunchShortcut { + + /** + * Launch configuration id of the configs created by this shortcut. + */ + public static final String LAUNCH_CONFIG_TYPE_ID = BootLaunchConfigurationDelegate.TYPE_ID; + + @Override + public IType[] findTypes(Object[] elements, IRunnableContext context) + throws InterruptedException, CoreException { + //For spring boot app, instead of searching for a main type in the entire project and all its + // libraries... try to look inside the project's pom for the corresponding property. + for (Object e : elements) { + if (e instanceof IProject) { + try { + e = JavaCore.create((IProject)e); + } catch (Throwable ignore) { + } + } + { + IType type = isMainMethod(elements[0]); + if(type != null) { + return new IType[] {type}; + } + } + if (e instanceof IJavaElement) { + if (e instanceof IType) { + if (hasMainMethod((IType) e)) { + return new IType[] {(IType)e}; + } + } + if (e instanceof ICompilationUnit) { + for (IType t : ((ICompilationUnit) e).getAllTypes()) { + if (hasMainMethod(t)) { + return new IType[] {t}; + } + } + } + final IJavaProject jp = ((IJavaElement)e).getJavaProject(); + final IType[][] result = new IType[][] { null }; + try { + context.run(/*fork*/false, /*true*/false, new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + try { + result[0] = MainTypeFinder.guessMainTypes(jp, monitor); + } catch (CoreException e) { + throw new InvocationTargetException(e); + } + } + }); + } catch (InvocationTargetException exception) { + throw ExceptionUtil.coreException(exception); + } + return result[0]; + } + } + //This isn't the best thing to to do as it searches also in all the library jars for main types. But it is + // only a fallback option if the above code failed. (Or should we rather signal an error instead?) + return super.findTypes(elements, context); + } + + private boolean hasMainMethod(IType t) { + try { + for (IMethod m : t.getMethods()) { + if (m.isMainMethod()) { + return true; + } + } + } catch (Exception e) { + BootActivator.log(e); + } + return false; + } + + /** + * Returns the smallest enclosing IType if the specified object is a main method, or null + * @param o the object to inspect + * @return the smallest enclosing IType of the specified object if it is a main method or null if it is not + */ + private IType isMainMethod(Object o) { + if(o instanceof IAdaptable) { + IAdaptable adapt = (IAdaptable) o; + IJavaElement element = (IJavaElement) adapt.getAdapter(IJavaElement.class); + if(element != null && element.getElementType() == IJavaElement.METHOD) { + try { + IMethod method = (IMethod) element; + if(method.isMainMethod()) { + return method.getDeclaringType(); + } + } + catch (JavaModelException jme) {JDIDebugUIPlugin.log(jme);} + } + } + return null; + } + + @Override + protected ILaunchConfigurationType getConfigurationType() { + return getLaunchManager().getLaunchConfigurationType(LAUNCH_CONFIG_TYPE_ID); + } + + /** + * Overridden, copied and changed to alter the generated launch configuration name. + */ + @Override + public ILaunchConfiguration createConfiguration(IType type) { + ILaunchConfiguration config = null; + try { + config = BootLaunchConfigurationDelegate.createConf(type); + } catch (CoreException exception) { + MessageDialog.openError(JDIDebugUIPlugin.getActiveWorkbenchShell(), LauncherMessages.JavaLaunchShortcut_3, exception.getStatus().getMessage()); + } + return config; + } + + /** + * Returns the singleton launch manager. + * + * @return launch manager + */ + private ILaunchManager getLaunchManager() { + return DebugPlugin.getDefault().getLaunchManager(); + } + + public static void launch(IProject project, String mode) { + BootLaunchShortcut shortcut = new BootLaunchShortcut(); + StructuredSelection selection = new StructuredSelection(new Object[] {project}); + shortcut.launch(selection, mode); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchUIModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchUIModel.java new file mode 100644 index 000000000..79347a1f4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchUIModel.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.launch; + +import static org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate.DEFAULT_ENABLE_DEBUG_OUTPUT; +import static org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate.ENABLE_DEBUG_OUTPUT; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.ANSI_CONSOLE_OUTPUT; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.DEFAULT_HIDE_FROM_BOOT_DASH; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.FAST_STARTUP; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; + +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.*; + +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.core.BootPreferences; +import org.springframework.ide.eclipse.boot.core.SpringBootCore; +import org.springframework.ide.eclipse.boot.launch.livebean.EnableJmxFeaturesModel; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; + +/** + * Model for the 'main type' selection widgetry on a launchconfiguration tab. + *

+ * Contains the 'logic' for the UI except for the widgets themselves. + * Can be unit tested without having to instantiate launch configuration dialogs + * etc. + * + * @author Kris De Volder + */ +public class BootLaunchUIModel { + + // TODO pulling out all of the logic for regression testing is a work in progress. + // Only some of the UI elements are represented in here. The other ones + // still are 'tangled' with the UI widgetry code. + + public static class MainTypeValidator extends Validator { + + private LiveVariable mainTypeName; + + public MainTypeValidator(LiveVariable n) { + this.mainTypeName = n; + dependsOn(mainTypeName); + } + + protected ValidationResult compute() { + String name = mainTypeName.getValue(); + if (!StringUtil.hasText(name)) { + return ValidationResult.error("No Main type selected"); + } + return ValidationResult.OK; + } + } + + public final SelectProjectLaunchTabModel project; + public final MainTypeNameLaunchTabModel mainTypeName; + public final ProfileLaunchTabModel profile; + public final LaunchTabSelectionModel enableDebug; + public final EnableJmxFeaturesModel enableJmx; + public final LaunchTabSelectionModel hideFromDash; + public final LaunchTabSelectionModel ansiConsoleOutput; + public final LaunchTabSelectionModel fastStartup; + public final LaunchTabSelectionModel useThinWrapper; + + public BootLaunchUIModel(IProfileHistory profileHistory) { + project = SelectProjectLaunchTabModel.create(); + mainTypeName = MainTypeNameLaunchTabModel.create(); + profile = ProfileLaunchTabModel.create(project.selection, profileHistory); + enableDebug = CheckboxLaunchTabModel.create(ENABLE_DEBUG_OUTPUT, DEFAULT_ENABLE_DEBUG_OUTPUT); + enableJmx = new EnableJmxFeaturesModel(); + hideFromDash = CheckboxLaunchTabModel.create(HIDE_FROM_BOOT_DASH, DEFAULT_HIDE_FROM_BOOT_DASH); + ansiConsoleOutput = CheckboxLaunchTabModel.create(ANSI_CONSOLE_OUTPUT, BootLaunchConfigurationDelegate.supportsAnsiConsoleOutput()); + fastStartup = CheckboxLaunchTabModel.create(FAST_STARTUP, BootActivator.getDefault().getPreferenceStore() + .getBoolean(BootPreferences.PREF_BOOT_FAST_STARTUP_DEFAULT)); + Validator thinWrapperValidator = new Validator() { + @Override + protected ValidationResult compute() { + IProject p = project.selection.getValue(); + if (p!=null && p.isAccessible() && useThinWrapper!=null) { + Boolean useThinWrapperValue = useThinWrapper.selection.getValue(); + if (useThinWrapperValue!=null && useThinWrapperValue) { + try { + if (!p.hasNature(SpringBootCore.M2E_NATURE)) { + return ValidationResult.error("Thin launcher support only works for Maven projects"); + } + } catch (CoreException e) { + Log.log(e); + } + } + } + return ValidationResult.OK; + } + }; + useThinWrapper = CheckboxLaunchTabModel.create(USE_THIN_WRAPPER, DEFAULT_USE_THIN_WRAPPER, thinWrapperValidator); + thinWrapperValidator.dependsOn(useThinWrapper.selection); + thinWrapperValidator.dependsOn(project.selection); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootMainTab.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootMainTab.java new file mode 100644 index 000000000..ae5ec28a2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootMainTab.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2015, 2018 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.launch; + +import static org.springframework.ide.eclipse.boot.ui.BootUIImages.BOOT_ICON; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.boot.core.BootPreferences; +import org.springframework.ide.eclipse.boot.launch.livebean.EnableJmxSection; +import org.springframework.ide.eclipse.boot.launch.profiles.ProfileHistory; +import org.springframework.ide.eclipse.boot.launch.profiles.ProfileLaunchTabSection; +import org.springframework.ide.eclipse.boot.launch.properties.PropertiesEditorSection; +import org.springframework.ide.eclipse.boot.launch.util.DelegatingLaunchConfigurationTabSection; +import org.springframework.ide.eclipse.boot.launch.util.GroupLaunchTabSection; +import org.springframework.ide.eclipse.boot.launch.util.LaunchConfigurationTabWithSections; +import org.springframework.ide.eclipse.boot.ui.BootUIImages; +import org.springsource.ide.eclipse.commons.livexp.ui.CheckboxSection; +import org.springsource.ide.eclipse.commons.livexp.ui.HLineSection; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.WizardPageSection; + +/** + * @author Kris De Volder + */ +public class BootMainTab extends LaunchConfigurationTabWithSections implements IPageWithSections { + + @Override + public String getName() { + return "Spring Boot"; + } + + @Override + public Image getImage() { + return BootUIImages.getImage(BOOT_ICON); + } + + @Override + protected List createSections() { + BootLaunchUIModel model = new BootLaunchUIModel(new ProfileHistory()); + List jvmArgsSections = new ArrayList<>(4); + jvmArgsSections.add(new EnableDebugSection(this, model.enableDebug)); + jvmArgsSections.add(new HideFromBootDashSection(this, model.hideFromDash)); + jvmArgsSections.add(new FastStartupLaunchTabSection(this, model.fastStartup)); + /* + * Show UI for enabling/disabling ANSI console output only if + * IDE supports ANSI console output + */ + if (BootLaunchConfigurationDelegate.supportsAnsiConsoleOutput()) { + jvmArgsSections.add(new DelegatingLaunchConfigurationTabSection(this, model.ansiConsoleOutput, new CheckboxSection(this, model.ansiConsoleOutput, "ANSI console output"))); + } + if (BootPreferences.getInstance().getThinWrapper()!=null) { + jvmArgsSections.add(new UseThinWrapperSection(this, model.useThinWrapper)); + } + + return Arrays.asList(new IPageSection[] { + SelectProjectLaunchTabSection.create(this, model.project), + new MainTypeLaunchTabSection(this, model.project.selection, model.mainTypeName), + new ProfileLaunchTabSection(this, model.profile), + new HLineSection(this), + new GroupLaunchTabSection(this, null, jvmArgsSections.toArray(new WizardPageSection[jvmArgsSections.size()])).columns(2), + new EnableJmxSection(this, model.enableJmx), + new HLineSection(this), + new PropertiesEditorSection(this, model.project.selection) + }); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootMavenClassPathProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootMavenClassPathProvider.java new file mode 100644 index 000000000..675d888c9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootMavenClassPathProvider.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2017, 2020 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.launch; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.launching.IRuntimeClasspathEntry; +import org.eclipse.m2e.jdt.internal.launch.MavenRuntimeClasspathProvider; + +@SuppressWarnings("restriction") +public class BootMavenClassPathProvider extends MavenRuntimeClasspathProvider { + + @Override + public IRuntimeClasspathEntry[] resolveClasspath(IRuntimeClasspathEntry[] entries, + ILaunchConfiguration configuration) throws CoreException { + return super.resolveClasspath(entries, BootLaunchConfigurationDelegate.copyAs(configuration, JDT_JAVA_APPLICATION)); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootMavenSourcePathProvider.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootMavenSourcePathProvider.java new file mode 100644 index 000000000..7911fdecd --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootMavenSourcePathProvider.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2015, 2020 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.launch; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.launching.IRuntimeClasspathEntry; +import org.eclipse.m2e.jdt.internal.launch.MavenSourcePathProvider; + +@SuppressWarnings("restriction") +public class BootMavenSourcePathProvider extends MavenSourcePathProvider { + + @Override + public IRuntimeClasspathEntry[] resolveClasspath(IRuntimeClasspathEntry[] entries, + ILaunchConfiguration configuration) throws CoreException { + return super.resolveClasspath(entries, BootLaunchConfigurationDelegate.copyAs(configuration, JDT_JAVA_APPLICATION)); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/CheckboxLaunchTabModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/CheckboxLaunchTabModel.java new file mode 100644 index 000000000..59ca093d9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/CheckboxLaunchTabModel.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.launch; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; + +/** + * Basic model for a checkbox who's value is stored into a launch configuration attribute. + * + * @author Kris De Volder + */ +public class CheckboxLaunchTabModel extends LaunchTabSelectionModel { + + public static CheckboxLaunchTabModel create(String attributeName, boolean defaultValue, LiveExpression validator) { + LiveVariable enable = new LiveVariable<>(); + return new CheckboxLaunchTabModel(attributeName, defaultValue, enable, validator); + } + + public static CheckboxLaunchTabModel create(String attributeName, boolean defaultValue) { + LiveVariable enable = new LiveVariable<>(); + return new CheckboxLaunchTabModel(attributeName, defaultValue, enable, Validator.OK); + } + + private String attributeName; + private boolean defaultValue; + + protected CheckboxLaunchTabModel(String attributeName, boolean defaultValue, LiveVariable selection, LiveExpression validator) { + super(selection, validator); + this.attributeName = attributeName; + this.defaultValue = defaultValue; + } + + @Override + public void initializeFrom(ILaunchConfiguration conf) { + selection.setValue(getAttribute(conf)); + getDirtyState().setValue(false); + } + + @Override + public void performApply(ILaunchConfigurationWorkingCopy conf) { + setAttribute(conf, selection.getValue()); + getDirtyState().setValue(false); + } + + @Override + public void setDefaults(ILaunchConfigurationWorkingCopy conf) { + setAttribute(conf, defaultValue); + } + + protected boolean getAttribute(ILaunchConfiguration conf) { + try { + return conf.getAttribute(attributeName, defaultValue); + } catch (CoreException e) { + BootActivator.log(e); + } + return defaultValue; + } + + protected void setAttribute(ILaunchConfigurationWorkingCopy conf, Boolean value) { + conf.setAttribute(attributeName, value); + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/DeletedLaunchConfTerminator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/DeletedLaunchConfTerminator.java new file mode 100644 index 000000000..33154b3ce --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/DeletedLaunchConfTerminator.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2016 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.launch; + +import java.util.function.Predicate; + +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationListener; +import org.eclipse.debug.core.ILaunchManager; +import org.springframework.ide.eclipse.boot.util.Log; + +/** + * Listens for deleted launch configurations and checks for 'orphaned' launches. + * Any orphaned launches which meet the 'isInteresting' test are terminated. + * + * @author Kris De Volder + */ +public class DeletedLaunchConfTerminator implements ILaunchConfigurationListener { + + private ILaunchManager lm; + private final Predicate isInteresting; + + public DeletedLaunchConfTerminator(ILaunchManager lm, Predicate isInteresting) { + this.isInteresting = isInteresting; + this.lm = lm; + this.lm.addLaunchConfigurationListener(this); + } + + @Override + public void launchConfigurationAdded(ILaunchConfiguration configuration) { + //don't care + } + + @Override + public void launchConfigurationChanged(ILaunchConfiguration configuration) { + //don't care + } + + @Override + public void launchConfigurationRemoved(ILaunchConfiguration deletedConf) { + //Careful we are somewhat limited on what we can do with this config since it has been deleted. + System.out.println("Deleted conf: "+deletedConf); + for (ILaunch l : lm.getLaunches()) { + if (l.canTerminate() + && !l.isTerminated() + && isInteresting.test(l) + ) { + ILaunchConfiguration conf = l.getLaunchConfiguration(); + //Careful conf could be null (because it was deleted), or not. + //It depends on when we get here (race condition of some kind). + //I've seen it happen either way! So we have to handle both cases! + if (conf==null || deletedConf.equals(conf)) { + try { + l.terminate(); + } catch (Exception e) { + Log.log(e); + } + } + } + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/EnableDebugSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/EnableDebugSection.java new file mode 100644 index 000000000..c804b54dc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/EnableDebugSection.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch; + +import org.springframework.ide.eclipse.boot.launch.util.DelegatingLaunchConfigurationTabSection; +import org.springsource.ide.eclipse.commons.livexp.ui.CheckboxSection; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; + +/** + * @author Kris De Volder + */ +public class EnableDebugSection extends DelegatingLaunchConfigurationTabSection { + + public EnableDebugSection(IPageWithSections owner, LaunchTabSelectionModel model) { + super(owner, model, new CheckboxSection(owner, model, "Enable debug output")); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/ExistingBootProjectSelectionValidator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/ExistingBootProjectSelectionValidator.java new file mode 100644 index 000000000..d83ab588c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/ExistingBootProjectSelectionValidator.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2015 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch; + +import static org.springsource.ide.eclipse.commons.livexp.core.ValidationResult.error; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.Assert; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; + +/** + * Validation for a 'existing spring-boot-project selection'. + * + * @author Kris De Volder + */ +public class ExistingBootProjectSelectionValidator extends Validator { + + private LiveExpression selection; + + public ExistingBootProjectSelectionValidator(LiveExpression selection) { + Assert.isNotNull(selection); + this.selection = selection; + dependsOn(selection); + } + + @Override + protected ValidationResult compute() { + IProject p = selection.getValue(); + if (p==null) { + return error("No project selected"); + } else if (!p.exists()) { + return error("Project '"+p.getName()+"' does not exist in the workspace"); + } else if (!p.isAccessible()) { + return error("Project '"+p.getName()+"' is closed"); + } else if (!BootPropertyTester.isBootProject(p)) { + return error("Project '"+p.getName()+"' does not look like a Boot project"); + } + return ValidationResult.OK; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/FastStartupLaunchTabSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/FastStartupLaunchTabSection.java new file mode 100644 index 000000000..f802a423d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/FastStartupLaunchTabSection.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch; + +import org.eclipse.jface.dialogs.MessageDialogWithToggle; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.core.BootPreferences; +import org.springframework.ide.eclipse.boot.launch.util.DelegatingLaunchConfigurationTabSection; +import org.springsource.ide.eclipse.commons.livexp.core.FieldModel; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.SelectionModel; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.WizardPageSection; + +/** + * Fast startup UI section for the Boot launch configuration wizard + * + * @author Alex Boyko + * + */ +public class FastStartupLaunchTabSection extends DelegatingLaunchConfigurationTabSection { + + static class UI extends WizardPageSection { + + private String label; + private Button button; + + private SelectionModel model; + + public UI(IPageWithSections owner, FieldModel model) { + this(owner, new SelectionModel<>(model.getVariable(), model.getValidator()), model.getLabel()); + } + + public UI(IPageWithSections owner, SelectionModel model, String label) { + super(owner); + this.model = model; + this.label = label; + } + + @Override + public void createContents(Composite page) { + button = new Button(page, SWT.CHECK); + button.setText(label); + button.addSelectionListener(new SelectionAdapter() { + public void widgetSelected(SelectionEvent e) { + model.selection.setValue(button.getSelection()); + if (button.getSelection()) { + IPreferenceStore store = BootActivator.getDefault().getPreferenceStore(); + boolean remind = store.getBoolean(BootPreferences.PREF_BOOT_FAST_STARTUP_REMIND_MESSAGE); + if (remind) { + MessageDialogWithToggle.openWarning(e.display.getActiveShell(), "Fast Startup Warning", + "Fast startup performs Java VM arguments tuning to enhance startup at the possible expense of a performance of the application during its runtime. It is recommended to use this setting for debugging purposes only", + "Do not show this message again in the future", !remind, store, + BootPreferences.PREF_BOOT_FAST_STARTUP_REMIND_MESSAGE); + } + } + } + }); + model.selection.addListener(new ValueListener() { + public void gotValue(LiveExpression exp, Boolean selected) { + if (selected!=null) { + button.setSelection(selected); + } + } + }); + } + + @Override + public LiveExpression getValidator() { + return model.validator; + } + } + + public FastStartupLaunchTabSection(IPageWithSections owner, LaunchTabSelectionModel model) { + super(owner, model, new UI(owner, model, "Fast startup")); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/HideFromBootDashSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/HideFromBootDashSection.java new file mode 100644 index 000000000..a6b8e5ced --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/HideFromBootDashSection.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2016 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.launch; + +import org.springframework.ide.eclipse.boot.launch.util.DelegatingLaunchConfigurationTabSection; +import org.springsource.ide.eclipse.commons.livexp.ui.CheckboxSection; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; + +/** + * @author Kris De Volder + */ +public class HideFromBootDashSection extends DelegatingLaunchConfigurationTabSection { + + public HideFromBootDashSection(IPageWithSections owner, LaunchTabSelectionModel model) { + super(owner, model, new CheckboxSection(owner, model, "Hide from Boot Dash").grabHor(true)); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/IProfileHistory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/IProfileHistory.java new file mode 100644 index 000000000..e66a4e951 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/IProfileHistory.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch; + +import org.eclipse.core.resources.IProject; + +/** + * Provides some means to retrieve profile history for projects. I.e. a list of + * spring boot profiles that have been used to launch a given project in the past. + * + * @author Kris De Volder + */ +public interface IProfileHistory { + + String[] getHistory(IProject value); + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/IgnoreExceptionOfType.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/IgnoreExceptionOfType.java new file mode 100644 index 000000000..23b75d767 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/IgnoreExceptionOfType.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2016 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.launch; + +import java.util.ArrayList; + +import org.eclipse.debug.core.DebugEvent; +import org.eclipse.debug.core.DebugException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.IDebugEventFilter; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.model.IBreakpoint; +import org.eclipse.jdt.debug.core.IJavaExceptionBreakpoint; +import org.eclipse.jdt.debug.core.IJavaThread; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; + +/** + * An instance of this type causes a given type of exception to be 'ignored' by the debug UI. + * + * @author Kris De Volder + */ +public class IgnoreExceptionOfType implements IDebugEventFilter, Disposable { + + //This class works as follows: + // - an 'EventFilter is attached to the debug model. + // - if an event is detected that corresponds to suspending on an exception breakpoint of the 'ignored' type. + // => a) the event is'filtered' (so that it won't cause a popup to swithc to debugging perspective + // b) the suspended thread is automatically resumed (so that it looks to the user as if the breakpoint wasn't hit) + + // Why such a strange way of doing things? + + // Because these methods do not work: + // + // a) try to use a BreakPointListener instead. It didn't seem possible to suppress a breakpoint from being installed + // or the breakpoint from causing a suspension. (There's a voting mechanism, but its not possible to override a + // vote to install / suspend cast by another existing listener) + + // b) use a DebugEventListener (instead of a filter). This does work, but it has the annoying effect that, + // because breakpoint is actually triggered and then auto-resumed, the debug UI still pops up a dialog + // to switch into the debug perspective. The filter allows us to 'hide' that event. (note that just filtering + // the event isn't enough as this only stops the debug ui from reacting to the breakpoint being hit, but + // the thread still ends up suspended nevertheless) + +// private static final boolean DEBUG = true; +// +// private static void debug(String string) { +// if (DEBUG) { +// System.out.println(string); +// } +// } + + private ILaunch launch; + private String exceptionToIgnore; + + /** + * Create an instance and register it as a listened of the debug model. + */ + public IgnoreExceptionOfType(ILaunch launch, String exceptionToIgnore) { + this.launch = launch; + this.exceptionToIgnore = exceptionToIgnore; + DebugPlugin.getDefault().addDebugEventFilter(this); + } + + @Override + public void dispose() { + DebugPlugin.getDefault().removeDebugEventFilter(this); + } + + @Override + public DebugEvent[] filterDebugEvents(DebugEvent[] events) { + ArrayList filtered = new ArrayList<>(events.length); + for (DebugEvent e : events) { + if (select(e)) { + filtered.add(e); + } + } + if (!filtered.isEmpty()) { + return filtered.toArray(new DebugEvent[filtered.size()]); + } + return null; + } + + private boolean select(DebugEvent e) { + if (e.getKind()==DebugEvent.SUSPEND && e.getDetail()==DebugEvent.BREAKPOINT) { + IJavaThread source = (IJavaThread) e.getSource(); + if (launch==null || launch.equals(source.getLaunch())) { + IBreakpoint[] bps = source.getBreakpoints(); + if (bps!=null) { + for (IBreakpoint bp : bps) { + if (bp instanceof IJavaExceptionBreakpoint) { + IJavaExceptionBreakpoint ebp = (IJavaExceptionBreakpoint) bp; + if (exceptionToIgnore.equals(ebp.getExceptionTypeName())) { + resume(source); + return false; + } + } + } + } + } + } + return true; + } + + private void resume(IJavaThread source) { + try { + source.resume(); + } catch (DebugException e) { + BootActivator.log(e); + } + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/LaunchTabSelectionModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/LaunchTabSelectionModel.java new file mode 100644 index 000000000..3ec7718ee --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/LaunchTabSelectionModel.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch; + +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.springframework.ide.eclipse.boot.launch.util.ILaunchConfigurationTabModel; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.SelectionModel; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +/** + * Abstract base clase for a {@link SelectionModel} that also implements {@link ILaunchConfigurationTabModel} + *

+ * Note that this class manages the 'dirtyState' for the model but only partially. I.e. the state gets set + * to true when the model contents changes, but it is up to the subclass to set it to false whenever + * the model state is synched with {@link ILaunchConfigurationWorkingCopy} state. + * + * @author Kris De Volder + */ +public abstract class LaunchTabSelectionModel extends SelectionModel implements ILaunchConfigurationTabModel { + + private LiveVariable dirtyState = new LiveVariable(false); + + public LaunchTabSelectionModel(final LiveVariable selection, LiveExpression validator) { + super(selection, validator); + selection.addListener(new ValueListener() { + @Override + public void gotValue(LiveExpression exp, T value) { + getDirtyState().setValue(true); + } + }); + } + + public LaunchTabSelectionModel(SelectionModel selection) { + this(selection.selection, selection.validator); + } + + @Override + public final LiveExpression getValidator() { + return validator; + } + + @Override + public final LiveVariable getDirtyState() { + return dirtyState; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/MainTypeLaunchTabSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/MainTypeLaunchTabSection.java new file mode 100644 index 000000000..192fbcdfe --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/MainTypeLaunchTabSection.java @@ -0,0 +1,201 @@ +/******************************************************************************* + * Copyright (c) 2005, 2012, 2015 IBM Corporation and others. + * 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: + * IBM Corporation - initial API and implementation + * Pivotal Software - Bits and pieces copied to create MainTypeSelectionSection + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.debug.internal.ui.SWTFactory; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.search.IJavaSearchScope; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.internal.debug.ui.JDIDebugUIPlugin; +import org.eclipse.jdt.internal.debug.ui.launcher.DebugTypeSelectionDialog; +import org.eclipse.jdt.internal.debug.ui.launcher.LauncherMessages; +import org.eclipse.jdt.internal.debug.ui.launcher.MainMethodSearchEngine; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.launch.util.DelegatingLaunchConfigurationTabSection; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.SelectionModel; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.UIConstants; +import org.springsource.ide.eclipse.commons.livexp.ui.WizardPageSection; + +/** + * A LaunchConfigurationTabSection hat allows user to choose main type + * in a project. + *

+ * Basically a simplified version to the JDT JavaMainTab. some of the + * rarely used options have been removed and the code converted + * to be useable as a LaunchConfigurationTabSection so it can be + * composed more freely with other LaunchTabSectons onto a page. + * + * @author Kris De Volder + */ +@SuppressWarnings("restriction") +public class MainTypeLaunchTabSection extends DelegatingLaunchConfigurationTabSection { + + public MainTypeLaunchTabSection(IPageWithSections owner, LiveVariable project, MainTypeNameLaunchTabModel model) { + super(owner, model, createUi(owner, project, model)); + } + + private static IPageSection createUi(IPageWithSections owner, final LiveVariable project, final SelectionModel mainTypeName) { + return new WizardPageSection(owner) { + + private Text fMainText; + private Button fSearchButton; + + private LiveVariable mainTypeName() { + return mainTypeName.selection; + } + private LiveVariable project() { + return project; + } + + @Override + public LiveExpression getValidator() { + return mainTypeName.validator; + } + + public void createContents(Composite parent) { + String label = "Main type"; + GridDataFactory grabHor = GridDataFactory.fillDefaults().grab(true, false); + + Composite field = new Composite(parent, SWT.NONE); + GridLayout layout = GridLayoutFactory.fillDefaults().numColumns(3).create(); +// GridLayout layout = new GridLayout(); + // layout.numColumns = 3; +// layout.marginBottom = 0; +// layout.marginTop = 0; +// layout.marginLeft = 0; +// layout.marginRight = 0; +// layout.marginWidth = 0; + field.setLayout(layout); + grabHor.applyTo(field); + + Label fieldNameLabel = new Label(field, SWT.NONE); + fieldNameLabel.setText(label); + GridDataFactory.fillDefaults() + .hint(UIConstants.fieldLabelWidthHint(fieldNameLabel), SWT.DEFAULT) + .align(SWT.BEGINNING, SWT.CENTER) + .applyTo(fieldNameLabel); + + fMainText = new Text(field, SWT.BORDER); + grabHor.applyTo(fMainText); + fMainText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mainTypeName().setValue(fMainText.getText()); + } + }); + mainTypeName().addListener(new ValueListener() { + public void gotValue(LiveExpression exp, String newName) { + if (fMainText!=null) { + if (newName!=null) { + String oldName = fMainText.getText(); + //Don't set the text if its not actually changed. + // Otherwise change events from typing in the widget will cause the + // text to be set and this (on Mac OS) cause the widget's text selection + // to be reset as well. + if (!oldName.equals(newName)) { + fMainText.setText(newName); + } + } + } + } + }); + fSearchButton = SWTFactory.createPushButton(field, "Search...", null); + fSearchButton.addSelectionListener(new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + } + public void widgetSelected(SelectionEvent e) { + handleSearchButtonSelected(); + } + }); + } + + /** + * Show a dialog that lists all main types + */ + protected void handleSearchButtonSelected() { + IJavaProject project = getJavaProject(); + IJavaElement[] elements = null; + if ((project == null) || !project.exists()) { + IJavaModel model = JavaCore.create(ResourcesPlugin.getWorkspace().getRoot()); + if (model != null) { + try { + elements = model.getJavaProjects(); + } + catch (JavaModelException e) {JDIDebugUIPlugin.log(e);} + } + } + else { + elements = new IJavaElement[]{project}; + } + if (elements == null) { + elements = new IJavaElement[]{}; + } + int constraints = IJavaSearchScope.SOURCES; + IJavaSearchScope searchScope = SearchEngine.createJavaSearchScope(elements, constraints); + MainMethodSearchEngine engine = new MainMethodSearchEngine(); + IType[] types = null; + try { + types = engine.searchMainMethods(owner.getRunnableContext(), searchScope, false); + } + catch (Exception e) { + BootActivator.log(e); + return; + } + DebugTypeSelectionDialog mmsd = new DebugTypeSelectionDialog(owner.getShell(), types, LauncherMessages.JavaMainTab_Choose_Main_Type_11); + if (mmsd.open() == Window.CANCEL) { + return; + } + Object[] results = mmsd.getResult(); + IType type = (IType)results[0]; + if (type != null) { + fMainText.setText(type.getFullyQualifiedName()); + project().setValue(type.getJavaProject().getProject()); + } + } + + private IJavaProject getJavaProject() { + try { + return JavaCore.create(project().getValue()); + } catch (Exception e) { + BootActivator.log(e); + return null; + } + } + }; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/MainTypeNameLaunchTabModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/MainTypeNameLaunchTabModel.java new file mode 100644 index 000000000..0c145c5c2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/MainTypeNameLaunchTabModel.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2015 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.launch.BootLaunchUIModel.MainTypeValidator; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; + +/** + * @author Kris De Volder + */ +public class MainTypeNameLaunchTabModel extends LaunchTabSelectionModel { + + private MainTypeNameLaunchTabModel(LiveVariable selection, + LiveExpression validator) { + super(selection, validator); + } + + public static MainTypeNameLaunchTabModel create() { + LiveVariable n = new LiveVariable<>(""); + MainTypeValidator nv = new MainTypeValidator(n); + return new MainTypeNameLaunchTabModel(n, nv); + } + + @Override + public void initializeFrom(ILaunchConfiguration config) { + try { + selection.setValue(config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, "")); + getDirtyState().setValue(false); + } catch (Exception e) { + BootActivator.log(e); + } + } + + public void performApply(ILaunchConfigurationWorkingCopy config) { + BootLaunchConfigurationDelegate.setMainType(config, StringUtil.trim(selection.getValue())); + getDirtyState().setValue(false); + } + + public void setDefaults(ILaunchConfigurationWorkingCopy config) { + BootLaunchConfigurationDelegate.setMainType(config, ""); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/ProfileLaunchTabModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/ProfileLaunchTabModel.java new file mode 100644 index 000000000..ee5b45cd9 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/ProfileLaunchTabModel.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch; + +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.DEFAULT_PROFILE; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.getProfile; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.setProfile; + +import java.util.LinkedHashSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.util.JavaProjectUtil; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; + +/** + * @author Kris De Volder + */ +public class ProfileLaunchTabModel extends LaunchTabSelectionModel { + + private static final Pattern FILE_NAME_PAT = Pattern.compile("^application-(.*)\\.properties$"); + private static final int FILE_NAME_PAT_GROUP = 1; + private static final String[] NO_PROFILES = new String[0]; + + public static ProfileLaunchTabModel create(LiveExpression project, IProfileHistory profileHistory) { + LiveVariable profile = new LiveVariable(""); + return new ProfileLaunchTabModel(project, profile, Validator.OK, profileHistory); + } + + private final LiveExpression profiles; + + protected ProfileLaunchTabModel(LiveExpression project, LiveVariable selection, LiveExpression validator, IProfileHistory profileHistory) { + super(selection, validator); + profiles = new ProfileOptions(project, profileHistory); + } + + @Override + public void initializeFrom(ILaunchConfiguration conf) { + String profile = getProfile(conf); + selection.setValue(profile); + getDirtyState().setValue(false); + } + + @Override + public void performApply(ILaunchConfigurationWorkingCopy conf) { + String profile = selection.getValue(); + setProfile(conf, profile); + //Note: it seems logical to update profile history here, but it gets called + // too often. I.e. not just when the user presses apply button, but really + // any time a change happens in the launch tab, the LaunchConfig editor copies + // all the data from the ui to a workingcopy by calling performApply. + //Therefore, history is instead updated when a launch config is actually + //launched. (See launch method in BootLaunchConfigurationDelegate) + getDirtyState().setValue(false); + } + + @Override + public void setDefaults(ILaunchConfigurationWorkingCopy conf) { + setProfile(conf, DEFAULT_PROFILE); + } + + /** + * Live expression that computes list of profiles to show in pull-down menu. + */ + public LiveExpression profileOptions() { + return profiles; + } + + /** + * LiveExpression that computes list of suggested profiles for the selected + * project. + * + * @author Kris De Volder + */ + private class ProfileOptions extends LiveExpression { + + private final IProfileHistory profileHistory; + private final LiveExpression project; + + public ProfileOptions(LiveExpression project, IProfileHistory profileHistory) { + super(NO_PROFILES); + this.profileHistory = profileHistory; + this.project = project; + dependsOn(project); + } + + @Override + protected String[] compute() { + LinkedHashSet profiles = new LinkedHashSet(); + discoverValidProfiles(profiles); + addHistoricProfiles(profiles); + return profiles.toArray(new String[profiles.size()]); + } + + /** + * Retrieve a stored list of profiles that have been used in the + * past with the selected project. Add these profiles to + * the provided List. + */ + private void addHistoricProfiles(LinkedHashSet profiles) { + IProject proj = project.getValue(); + if (proj!=null) { + for (String profile : profileHistory.getHistory(proj)) { + profiles.add(profile); + } + } + } + + /** + * @param profiles discovered profiles are added to this array. + */ + private void discoverValidProfiles(LinkedHashSet profiles) { + try { + for (IContainer srcFolder : JavaProjectUtil.getSourceFolders(project.getValue())) { + for (IResource rsrc : srcFolder.members()) { + if (rsrc.getType()==IResource.FILE) { + String name = rsrc.getName(); + Matcher matcher = FILE_NAME_PAT.matcher(name); + if (matcher.matches()) { + profiles.add(matcher.group(FILE_NAME_PAT_GROUP)); + } + } + } + } + } catch (Exception e) { + BootActivator.log(e); + } + } + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/SelectProjectLaunchTabModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/SelectProjectLaunchTabModel.java new file mode 100644 index 000000000..8dd2f46aa --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/SelectProjectLaunchTabModel.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch; + +import java.util.ArrayList; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; + +public class SelectProjectLaunchTabModel extends LaunchTabSelectionModel { + + public static SelectProjectLaunchTabModel create() { + LiveVariable project = new LiveVariable(); + ExistingBootProjectSelectionValidator validator = new ExistingBootProjectSelectionValidator(project); + return new SelectProjectLaunchTabModel(project, validator); + } + + public SelectProjectLaunchTabModel(LiveVariable p, + LiveExpression pv) { + super(p, pv); + } + + @Override + public void initializeFrom(ILaunchConfiguration conf) { + selection.setValue(BootLaunchConfigurationDelegate.getProject(conf)); + getDirtyState().setValue(false); + } + + @Override + public void performApply(ILaunchConfigurationWorkingCopy conf) { + BootLaunchConfigurationDelegate.setProject(conf, selection.getValue()); + getDirtyState().setValue(false); + } + + @Override + public void setDefaults(ILaunchConfigurationWorkingCopy conf) { + BootLaunchConfigurationDelegate.setProject(conf, null); + } + + public IProject[] interestingProjects() { + IProject[] allProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + ArrayList interesting = new ArrayList(allProjects.length); + for (IProject p : allProjects) { + if (isInteresting(p)) { + interesting.add(p); + } + } + return interesting.toArray(new IProject[interesting.size()]); + } + + /** + * Decides whether given IProject from the workspace is of interest. + * Only projects 'of interest' will be available from the project + * selector's pull-down menu. + */ + protected boolean isInteresting(IProject project) { + return BootPropertyTester.isBootProject(project); + } + + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/SelectProjectLaunchTabSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/SelectProjectLaunchTabSection.java new file mode 100644 index 000000000..1b387cbe2 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/SelectProjectLaunchTabSection.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch; + +import org.eclipse.core.resources.IProject; +import org.springframework.ide.eclipse.boot.launch.util.DelegatingLaunchConfigurationTabSection; +import org.springframework.ide.eclipse.boot.launch.util.ILaunchConfigurationTabSection; +import org.springsource.ide.eclipse.commons.livexp.ui.ChooseOneSectionCombo; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.SimpleLabelProvider; + +/** + * @author Kris De Volder + */ +public class SelectProjectLaunchTabSection { + + public static ILaunchConfigurationTabSection create(IPageWithSections owner, SelectProjectLaunchTabModel model) { + ChooseOneSectionCombo ui = + new ChooseOneSectionCombo(owner, "Project", model, model.interestingProjects()); + //allowTextEdits(ProjectNameParser.INSTANCE); + ui.setLabelProvider(new SimpleLabelProvider() { + public String getText(Object element) { + if (element instanceof IProject) { + return ((IProject) element).getName(); + } + return super.getText(element); + } + }); + return new DelegatingLaunchConfigurationTabSection(owner, model, ui); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/UseThinWrapperSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/UseThinWrapperSection.java new file mode 100644 index 000000000..2854dd2b4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/UseThinWrapperSection.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch; + +import org.springframework.ide.eclipse.boot.launch.util.DelegatingLaunchConfigurationTabSection; +import org.springsource.ide.eclipse.commons.livexp.ui.CheckboxSection; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; + +/** + * @author Kris De Volder + */ +public class UseThinWrapperSection extends DelegatingLaunchConfigurationTabSection { + + public UseThinWrapperSection(IPageWithSections owner, LaunchTabSelectionModel model) { + super(owner, model, new CheckboxSection(owner, model, "Use Thin Wrapper")); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootCliLaunchConfigurationDelegate.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootCliLaunchConfigurationDelegate.java new file mode 100644 index 000000000..7d65cae2c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootCliLaunchConfigurationDelegate.java @@ -0,0 +1,201 @@ +/******************************************************************************* + * Copyright (c) 2013, 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch.cli; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.model.LaunchConfigurationDelegate; +import org.eclipse.jdt.launching.IVMInstall; +import org.eclipse.jdt.launching.IVMRunner; +import org.eclipse.jdt.launching.JavaRuntime; +import org.eclipse.jdt.launching.VMRunnerConfiguration; +import org.springframework.ide.eclipse.boot.core.cli.BootInstallManager; +import org.springframework.ide.eclipse.boot.core.cli.install.IBootInstall; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.util.Log; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; + +/** + * Launch Configuration delegate able to start/shut down Spring Boot CLI processes + * + * @author Kris De Volder + * @author Alex Boyko + * + */ +public class BootCliLaunchConfigurationDelegate extends LaunchConfigurationDelegate { + + private static String[] getSpringBootClasspath(ILaunchConfiguration conf, IBootInstall install) throws Exception { + File[] bootLibJars = install.getBootLibJars(); + List classpath = new ArrayList<>(2 + bootLibJars.length); + classpath.add("."); + Path extensions = install.getHome().toPath().resolve("lib/ext/"); + if (Files.exists(extensions)) { + classpath.add(extensions.toString()); + } + classpath.addAll(Arrays.stream(bootLibJars).map(jarFile -> jarFile.toString()).collect(Collectors.toList())); + return classpath.toArray(new String[classpath.size()]); + } + + /** + * Spring Boot CLI main class to launch (Ideally this shouldn't be overriden) + * @param install Spring Boot CLI install + * @param launch Eclipse's launch + * @param conf Launch configuration + * @return main type class name + * @throws Exception + */ + protected String getMainTypeName(IBootInstall install, ILaunch launch, ILaunchConfiguration conf) throws Exception { + return "org.springframework.boot.loader.JarLauncher"; + } + + /** + * Environment variables for Spring Boot CLI process + * @param install Spring Boot CLI install + * @param launch Eclipse's launch + * @param conf Launch configuration + * @return environment variables as array of strings, where each string looks like SPRING_HOME=/Users/me/spring-cli + * @throws Exception + */ + protected String[] getEnv(IBootInstall install, ILaunch launch, ILaunchConfiguration conf) throws Exception { + Map map = DebugPlugin.getDefault().getLaunchManager().getNativeEnvironment(); + IVMInstall vmInstall = verifyVMInstall(conf); + map.put("JAVA_HOME", vmInstall.getInstallLocation().toString()); + try { + map.put("SPRING_HOME", install.getHome().toString()); + } catch (Exception e) { + Log.log(e); + } + List env = new ArrayList<>(map.size()); + String var = null; + for(Iterator iter = map.keySet().iterator(); iter.hasNext();) { + var = iter.next(); + String value = map.get(var); + if (value == null) { + value = ""; //$NON-NLS-1$ + } + if (var.equalsIgnoreCase("path")) { //$NON-NLS-1$ +// if(value.indexOf(jrestr) == -1) { +// value = jrestr+';'+value; +// } + continue; + } + env.add(var+"="+value); //$NON-NLS-1$ + } + + return env.toArray(new String[env.size()]); + } + + /** + * VM arguments for launching the Spring Boot CLI + * @param install Spring Boot CLI install + * @param launch Eclipse's launch + * @param conf Launch configuration + * @return array of VM arguments to launch the CLI + * @throws Exception + */ + protected String[] getVmArgs(IBootInstall install, ILaunch launch, ILaunchConfiguration conf) throws Exception { + if (BootLaunchConfigurationDelegate.supportsAnsiConsoleOutput()) { + return new String[] {"-Dspring.output.ansi.enabled=always"}; + } else { + return new String[0]; + } + } + + /** + * Specific VM attributes for launching Spring Boot CLI + * @param install Spring Boot CLI install + * @param launch Eclipse's launch + * @param conf Launch configuration + * @return specific VM attributes map + * @throws Exception + */ + protected Map getVmSpecificAttributesMap(IBootInstall install, ILaunch launch, ILaunchConfiguration conf) throws Exception { + return Collections.emptyMap(); + } + + /** + * Program arguments for the Spring Boot CLI. This is the typically overriden method to specify the spring cloud command, i.e. ["cloud", "--version"] + * @param install Spring Boot CLI install + * @param launch Eclipse's launch + * @param conf Launch configuration + * @return Spring Boot CLI command as an array of strings + * @throws Exception + */ + protected String[] getProgramArgs(IBootInstall install, ILaunch launch, ILaunchConfiguration conf) throws Exception { + return new String[0]; + } + + /** + * Working directory for launching the Spring Boot CLI + * @param install Spring Boot CLI install + * @param launch Eclipse's launch + * @param conf Launch configuration + * @return working directory for the launch + * @throws Exception + */ + protected String getWorkingDirectory(IBootInstall install, ILaunch launch, ILaunchConfiguration conf) throws Exception { + return install.getHome().toString(); + } + + @Override + final public void launch(ILaunchConfiguration conf, String mode, + ILaunch launch, IProgressMonitor monitor) throws CoreException { + //TODO: some common things that Java launch configs do that this one does not (yet) do but probably should + // - offer to save unsaved files + // - check for errors in project + // - source locators (for debugging processes) + // - launching in debug mode + try { + IBootInstall install = BootInstallManager.getInstance().getDefaultInstall(); + IVMInstall vm = verifyVMInstall(conf); + IVMRunner runner = vm.getVMRunner(mode); + + String mainTypeName = getMainTypeName(install, launch, conf); + String[] classpath = getSpringBootClasspath(conf, install); + + VMRunnerConfiguration runConfiguration = new VMRunnerConfiguration(mainTypeName, classpath); + + runConfiguration.setProgramArguments(getProgramArgs(install, launch, conf)); + runConfiguration.setVMArguments(getVmArgs(install, launch, conf)); + runConfiguration.setWorkingDirectory(getWorkingDirectory(install, launch, conf)); + runConfiguration.setEnvironment(getEnv(install, launch, conf)); + runConfiguration.setVMSpecificAttributesMap(getVmSpecificAttributesMap(install, launch, conf)); + + runner.run(runConfiguration, launch, monitor); + + } catch (Exception e) { + throw ExceptionUtil.coreException(e); + } + } + + protected IVMInstall verifyVMInstall(ILaunchConfiguration conf) { + //Extremely simplistic implementation. Just gets the default JVM for this workspace. + //TODO: project specific JVM selection or maybe the JVM should be associated with + // spring boot installation. + return JavaRuntime.getDefaultVMInstall(); + } + +} + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootGroovyScriptLaunchConfigurationDelegate.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootGroovyScriptLaunchConfigurationDelegate.java new file mode 100644 index 000000000..1b986354b --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootGroovyScriptLaunchConfigurationDelegate.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) 2013, 2017 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch.cli; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.springframework.ide.eclipse.boot.core.cli.install.IBootInstall; + +public class BootGroovyScriptLaunchConfigurationDelegate extends BootCliLaunchConfigurationDelegate { + + public static final String ID = "org.springsource.ide.eclipse.boot.groovy.script.launch"; + + /* + Example of a commandline invocation of the spring boot runtime. This is what we + are to emulate in here: + + /usr/lib/jvm/java-7-oracle/bin/java + -cp + .:/home/kdvolder/Applications/spring-0.5.0.M6/bin:/home/kdvolder/Applications/spring-0.5.0.M6/lib/spring-boot-cli-0.5.0.M6.jar + org.springframework.boot.loader.JarLauncher + run + app.groovy + + */ + + private static final String SCRIPT_RSRC = "spring.groovy.script.rsrc"; + + public static void setScript(ILaunchConfigurationWorkingCopy wc, IFile rsrc) { + wc.setAttribute(SCRIPT_RSRC, rsrc.getFullPath().toString()); + } + + private static IFile getScript(ILaunchConfiguration conf) throws CoreException { + String fullPathStr = conf.getAttribute(SCRIPT_RSRC, (String)null); + if (fullPathStr!=null) { + IPath fullPath = new Path(fullPathStr); + Assert.isLegal(fullPath.segmentCount()>=2); + String projectName = fullPath.segment(0); + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); + return project.getFile(fullPath.removeFirstSegments(1)); + } + return null; + } + + private static String getProjectName(ILaunchConfiguration conf) throws CoreException { + String fullPathStr = conf.getAttribute(SCRIPT_RSRC, (String)null); + if (fullPathStr!=null) { + IPath fullPath = new Path(fullPathStr); + return fullPath.segment(0); + } + return null; + } + + private static IProject getProject(ILaunchConfiguration conf) throws CoreException { + String name = getProjectName(conf); + if (name!=null) { + return ResourcesPlugin.getWorkspace().getRoot().getProject(name); + } + return null; + } + + @Override + protected String getWorkingDirectory(IBootInstall install, ILaunch launch, ILaunchConfiguration conf) + throws Exception { + IProject project = getProject(conf); + return project.getLocation().toFile().toString(); + } + + @Override + protected String[] getProgramArgs(IBootInstall install, ILaunch launch, ILaunchConfiguration conf) throws Exception { + return new String[] { + "run", + getScript(conf).getProjectRelativePath().toString() + }; + } + + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootGroovyScriptLaunchConfigurationTabGroup.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootGroovyScriptLaunchConfigurationTabGroup.java new file mode 100644 index 000000000..0e0a1b04d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootGroovyScriptLaunchConfigurationTabGroup.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2013, 2017 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch.cli; + +import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup; +import org.eclipse.debug.ui.ILaunchConfigurationDialog; +import org.eclipse.debug.ui.ILaunchConfigurationTab; + +public class BootGroovyScriptLaunchConfigurationTabGroup extends + AbstractLaunchConfigurationTabGroup { + + public BootGroovyScriptLaunchConfigurationTabGroup() { + } + + @Override + public void createTabs(ILaunchConfigurationDialog dialog, String mode) { + ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] { +// new SpringCommandTab() +// new AppletParametersTab(), +// new JavaArgumentsTab(), +// new JavaJRETab(), +// new JavaClasspathTab(), +// new CommonTab() + }; + setTabs(tabs); + + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootGroovyScriptLaunchShortcut.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootGroovyScriptLaunchShortcut.java new file mode 100644 index 000000000..96902f012 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/BootGroovyScriptLaunchShortcut.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2012, 2017 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch.cli; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.ui.DebugUITools; +import org.eclipse.debug.ui.ILaunchShortcut; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.springframework.ide.eclipse.boot.util.Log; + +public class BootGroovyScriptLaunchShortcut implements ILaunchShortcut { + + @Override + public void launch(ISelection selection, String mode) { + try { + IResource rsrc = getResource(selection); + launch(rsrc, mode); + } catch (Throwable e) { + Log.log(e); + } + } + + public void launch(IResource rsrc, String mode) throws CoreException { + if (rsrc!=null && rsrc.getType()==IResource.FILE) { + ILaunchConfiguration conf = createConfiguration((IFile) rsrc); + DebugUITools.launch(conf, mode); + } + } + + /** + * Returns the singleton launch manager. + * + * @return launch manager + */ + private ILaunchManager getLaunchManager() { + return DebugPlugin.getDefault().getLaunchManager(); + } + + protected ILaunchConfiguration createConfiguration(IFile rsrc) throws CoreException { + ILaunchConfigurationType configType = getConfigurationType(); + String projectName = rsrc.getProject().getName(); + ILaunchConfigurationWorkingCopy wc = configType.newInstance(null, getLaunchManager().generateLaunchConfigurationName(projectName+" "+rsrc.getName())); + BootGroovyScriptLaunchConfigurationDelegate.setScript(wc, rsrc); + wc.setMappedResources(new IResource[] {rsrc}); + //Normally you should call: + //config = wc.doSave(); + //But we skip it for now. The launch conf will not be saved so it will be 'transient'. + return wc; + } + + protected ILaunchConfigurationType getConfigurationType() { + return getLaunchManager().getLaunchConfigurationType(BootGroovyScriptLaunchConfigurationDelegate.ID); + } + + private IResource getResource(ISelection selection) { + if (selection instanceof IStructuredSelection) { + IStructuredSelection ss = (IStructuredSelection) selection; + Object el = ss.getFirstElement(); + if (el instanceof IResource) { + return (IResource) el; + } else if (el instanceof IAdaptable) { + return (IResource) ((IAdaptable) el).getAdapter(IResource.class); + } + } + return null; + } + + + @Override + public void launch(IEditorPart editor, String mode) { + try { + IEditorInput input = editor.getEditorInput(); + IResource rsrc = (IResource) input.getAdapter(IResource.class); + if (rsrc!=null) { + launch(rsrc, mode); + } + System.out.println(input); + } catch (Throwable e) { + Log.log(e); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/CloudCliServiceLaunchConfigurationDelegate.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/CloudCliServiceLaunchConfigurationDelegate.java new file mode 100644 index 000000000..e5bffe613 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/CloudCliServiceLaunchConfigurationDelegate.java @@ -0,0 +1,297 @@ +/******************************************************************************* + * Copyright (c) 2017, 2020 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch.cli; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.model.IProcess; +import org.eclipse.debug.core.model.RuntimeProcess; +import org.eclipse.jface.dialogs.MessageDialogWithToggle; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.PlatformUI; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.core.cli.BootInstallManager; +import org.springframework.ide.eclipse.boot.core.cli.install.CloudCliInstall; +import org.springframework.ide.eclipse.boot.core.cli.install.IBootInstall; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.livebean.JmxBeanSupport; +import org.springframework.ide.eclipse.boot.launch.process.BootProcessFactory; +import org.springframework.ide.eclipse.boot.launch.util.PortFinder; +import org.springframework.ide.eclipse.boot.util.version.Version; +import org.springframework.ide.eclipse.boot.util.version.VersionParser; +import org.springframework.ide.eclipse.boot.util.version.VersionRange; +import org.springsource.ide.eclipse.commons.core.util.ProcessUtils; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +/** + * Spring Cloud CLI service launch configuration + * + * @author Alex Boyko + * + */ +public class CloudCliServiceLaunchConfigurationDelegate extends BootCliLaunchConfigurationDelegate { + + private static final VersionRange SPRING_CLOUD_CLI_SINGLE_PROCESS_VERSION_RANGE = new VersionRange(new Version(1, 3, 0, null)); + private static final VersionRange SPRING_CLOUD_CLI_SUPPORTS_THIN_LAUNCH_PARAM = VersionParser.DEFAULT.parseRange("[1.3.0, 2.2.0)"); + + public final static String TYPE_ID = "org.springframework.ide.eclipse.boot.launch.cloud.cli.service"; + + public final static String ATTR_CLOUD_SERVICE_ID = "local-cloud-service-id"; + + private final static String PREF_DONT_SHOW_PLATFORM_WARNING = "org.springframework.ide.eclipse.boot.launch.cloud.cli.NotSupportedPlatform"; + private final static String PREF_DONT_SHOW_JRE_WARNING = "org.springframework.ide.eclipse.boot.launch.cloud.cli.JRE"; + private final static String PREF_DONT_SHOW_JDK_WARNING = "org.springframework.ide.eclipse.boot.launch.cloud.cli.JDK"; + + private List getCloudCliServiceLifeCycleVmArguments(ILaunchConfiguration configuration, int jmxPort) { + List vmArgs = new ArrayList<>(); + EnumSet enabled = BootLaunchConfigurationDelegate + .getEnabledJmxFeatures(configuration); + if (!enabled.isEmpty()) { + String enableLiveBeanArgs = JmxBeanSupport.jmxBeanVmArgs(jmxPort, enabled); + vmArgs.addAll(Arrays.asList(enableLiveBeanArgs.split("\n"))); + } + return vmArgs; + } + + protected String[] getProgramArgs(IBootInstall bootInstall, ILaunch launch, ILaunchConfiguration configuration) { + try { + CloudCliInstall cloudCliInstall = bootInstall.getExtension(CloudCliInstall.class); + if (cloudCliInstall == null) { + Log.error("No Spring Cloud CLI installation found"); + } else { + String serviceId = configuration.getAttribute(ATTR_CLOUD_SERVICE_ID, (String) null); + Version cloudCliVersion = cloudCliInstall.getVersion(); + List vmArgs = new ArrayList<>(); + List args = new ArrayList<>(); + + args.add(CloudCliInstall.COMMAND_PREFIX); + args.add(serviceId); + + if (cloudCliVersion != null && SPRING_CLOUD_CLI_SUPPORTS_THIN_LAUNCH_PARAM.match(cloudCliVersion)) { + args.add("--deployer=thin"); + } + + args.add("--"); + args.add("--logging.level.org.springframework.cloud.launcher.deployer=DEBUG"); + + // VM argument for the service log output + if (BootLaunchConfigurationDelegate.supportsAnsiConsoleOutput()) { + vmArgs.add("-Dspring.output.ansi.enabled=always"); + } + + if (CloudCliServiceLaunchConfigurationDelegate.SPRING_CLOUD_CLI_SINGLE_PROCESS_VERSION_RANGE.match(cloudCliVersion)) { + if (!vmArgs.isEmpty()) { + args.add("--spring.cloud.launcher.deployables." + serviceId + ".properties.spring.cloud.deployer.local.javaOpts=" + String.join(",", vmArgs)); + } + } else if (CloudCliInstall.CLOUD_CLI_JAVA_OPTS_SUPPORTING_VERSIONS.match(cloudCliVersion)) { + int jmxPort = getJmxPort(configuration); + // Set the JMX port for launch + launch.setAttribute(BootLaunchConfigurationDelegate.JMX_PORT, String.valueOf(jmxPort)); + vmArgs.addAll(getCloudCliServiceLifeCycleVmArguments(configuration, jmxPort)); + // Set the JMX port connection jvm args for the service + if (!vmArgs.isEmpty()) { + args.add("--spring.cloud.launcher.deployables." + serviceId + ".properties.JAVA_OPTS=" + String.join(",", vmArgs)); + } + } + return args.toArray(new String[args.size()]); + } + } catch (Exception e) { + Log.log(e); + } + return new String[0]; + } + + private int getJmxPort(ILaunchConfiguration configuration) { + int port = 0; + try { + port = Integer.parseInt(BootLaunchConfigurationDelegate.getJMXPort(configuration)); + } catch (Exception e) { + // ignore: bad data in launch config. + } + if (port == 0) { + try { + // slightly better than calling JmxBeanSupport.randomPort() + port = PortFinder.findFreePort(); + } catch (IOException e) { + Log.log(e); + } + } + return port; + } + + public static boolean isLocalCloudServiceLaunch(ILaunchConfiguration conf) { + try { + if (conf!=null) { + String type = conf.getType().getIdentifier(); + return TYPE_ID.equals(type); + } + } catch (Exception e) { + Log.log(e); + } + return false; + } + + public static ILaunchConfigurationWorkingCopy createLaunchConfig(String serviceId) throws CoreException { + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunchConfigurationType type = launchManager.getLaunchConfigurationType(TYPE_ID); + ILaunchConfigurationWorkingCopy config = type.newInstance(null, serviceId); + + // Set default config with life cycle tracking support because it should cover with life cycle tracking and without + BootLaunchConfigurationDelegate.setDefaults(config, null, null); + + config.setAttribute(ATTR_CLOUD_SERVICE_ID, serviceId); + + // Overwrite process factory class because for latest version of Cloud CLI life cycle tracking through JMX port is not available for services + BootLaunchConfigurationDelegate.setProcessFactory(config, CloudCliProcessFactory.class); + return config; + } + + public static boolean canUseLifeCycle(ILaunch launch) { + ILaunchConfiguration conf = launch.getLaunchConfiguration(); + return conf!=null && canUseLifeCycle(conf); + } + + public static boolean isSingleProcessServiceConfig(ILaunchConfiguration conf) { + try { + if (isCloudCliService(conf)) { + IBootInstall bootInstall = BootInstallManager.getInstance().getDefaultInstall(); + if (bootInstall != null) { + Version cloudCliVersion = bootInstall.getExtension(CloudCliInstall.class) == null ? null : bootInstall.getExtension(CloudCliInstall.class).getVersion(); + return SPRING_CLOUD_CLI_SINGLE_PROCESS_VERSION_RANGE.match(cloudCliVersion); + } + } + } catch (Exception e) { + // ignore + } + return false; + } + + public static boolean isCloudCliService(ILaunchConfiguration conf) { + try { + return TYPE_ID.equals(conf.getType().getIdentifier()); + } catch (CoreException e) { + // Ignore + } + return false; + } + + public static boolean canUseLifeCycle(ILaunchConfiguration conf) { + try { + if (!isCloudCliService(conf)) { + return false; + } + IBootInstall bootInstall = BootInstallManager.getInstance().getDefaultInstall(); + if (bootInstall == null) { + return false; + } + Version cloudCliVersion = bootInstall.getExtension(CloudCliInstall.class) == null ? null : bootInstall.getExtension(CloudCliInstall.class).getVersion(); + // Cloud CLI version below 1.2.0 and over 1.3.0 can't have JMX connection to cloud service hence life cycle should be disabled. + if (!canUseLifeCycle(cloudCliVersion)) { + return false; + } + return SPRING_CLOUD_CLI_SINGLE_PROCESS_VERSION_RANGE.match(cloudCliVersion) || BootLaunchConfigurationDelegate.getEnableLifeCycle(conf); + } catch (Exception e) { + // Ignore + } + return false; + } + + private static boolean canUseLifeCycle(Version cloudCliVersion) { + // Cloud CLI version below 1.2.0 and over 1.3.0 can't have JMX connection to cloud service hence life cycle should be disabled. + if (cloudCliVersion == null + || !CloudCliInstall.CLOUD_CLI_JAVA_OPTS_SUPPORTING_VERSIONS.match(cloudCliVersion) + || SPRING_CLOUD_CLI_SINGLE_PROCESS_VERSION_RANGE.match(cloudCliVersion)) { + return false; + } + return true; + } + + public static class CloudCliProcessFactory extends BootProcessFactory { + + @Override + public IProcess newProcess(ILaunch launch, Process process, String label, Map attributes) { + try { + IBootInstall bootInstall = BootInstallManager.getInstance().getDefaultInstall(); + if (bootInstall != null) { + Version cloudCliVersion = bootInstall.getExtension(CloudCliInstall.class) == null ? null : bootInstall.getExtension(CloudCliInstall.class).getVersion(); + if (CloudCliServiceLaunchConfigurationDelegate.isSingleProcessServiceConfig(launch.getLaunchConfiguration())) { + final IPreferenceStore store = BootActivator.getDefault().getPreferenceStore(); + // Set invalid PID initially thus if PID is failed to be calculated then set PID launch attribute to invalid PID to fallback to default non-JMX process tracking + long pid = -1; + try { + if (ProcessUtils.isLatestJdkForTools()) { + pid = ProcessUtils.getProcessID(process); + } else { + Log.warn("Old JDK version. Need latest JDK to make JMX connection to process using its PID"); + if (!store.getBoolean(PREF_DONT_SHOW_JDK_WARNING)) { + PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { + MessageDialogWithToggle dialog = MessageDialogWithToggle.openWarning( + Display.getCurrent().getActiveShell(), "Cloud CLI Service Info Limitation", + "Cloud service process life-cycle data is limited and port data is unavailable because STS runnning on an old JDK version. Point STS to the latest JDK and restart it to have complete service process life-cycle and port data", + "Don't show this message again", + false, null, null); + store.setValue(PREF_DONT_SHOW_JDK_WARNING, dialog.getToggleState()); + }); + } + } + } catch (NoClassDefFoundError e) { + Log.warn(e); + if (!store.getBoolean(PREF_DONT_SHOW_JRE_WARNING)) { + PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { + MessageDialogWithToggle dialog = MessageDialogWithToggle.openWarning( + Display.getCurrent().getActiveShell(), "Cloud CLI Service Info Limitation", + "Cloud service process life-cycle data is limited and port data is unavailable because STS is running on a JRE. Point it to a JDK and restart STS for complete service process life-cycle and port data", + "Don't show this message again", + false, null, null); + store.setValue(PREF_DONT_SHOW_JRE_WARNING, dialog.getToggleState()); + }); + } + } catch (UnsupportedOperationException e) { + Log.warn(e); + if (!store.getBoolean(PREF_DONT_SHOW_PLATFORM_WARNING)) { + PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { + MessageDialogWithToggle dialog = MessageDialogWithToggle.openWarning( + Display.getCurrent().getActiveShell(), "Cloud CLI Service Info Limitation", + "Cloud service process life-cycle data is limited and port data is unavailable on the current platform.", + "Don't show this message again", + false, null, null); + store.setValue(PREF_DONT_SHOW_PLATFORM_WARNING, dialog.getToggleState()); + }); + } + } + launch.setAttribute(BootLaunchConfigurationDelegate.PROCESS_ID, String.valueOf(pid)); + return new RuntimeProcess(launch, process, label, attributes); + } else if (canUseLifeCycle(cloudCliVersion)) { + return super.newProcess(launch, process, label, attributes); + } + } + } catch (Exception e) { + Log.log(e); + } + return new RuntimeProcess(launch, process, label, attributes); + } + + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/console/BootConsolePageParticipant.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/console/BootConsolePageParticipant.java new file mode 100644 index 000000000..eb286fbc4 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/console/BootConsolePageParticipant.java @@ -0,0 +1,193 @@ +/******************************************************************************* + * Copyright (c) 2012, 2015 Pivotal Software, Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Contributors: + * Pivotal Software, Inc. - initial API and implementation + ***********************************************************************************/ +package org.springframework.ide.eclipse.boot.launch.console; + +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.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.model.IProcess; +import org.eclipse.debug.internal.ui.views.console.ProcessConsole; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.ActionContributionItem; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IContributionItem; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.SubContributionManager; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleConstants; +import org.eclipse.ui.console.IConsolePageParticipant; +import org.eclipse.ui.part.IPageBookViewPage; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.devtools.BootDevtoolsClientLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.util.BootLaunchUtils; +import org.springframework.ide.eclipse.boot.ui.BootUIImages; +import org.springframework.ide.eclipse.boot.util.ProcessListenerAdapter; +import org.springframework.ide.eclipse.boot.util.ProcessTracker; +import org.springsource.ide.eclipse.commons.frameworks.core.ExceptionUtil; + +/** + * @author Kris De Volder + */ +@SuppressWarnings("restriction") +public class BootConsolePageParticipant implements IConsolePageParticipant { + + private class TerminateProcessAction extends Action { + @Override + public void run() { + try { + final IProcess process = console.getProcess(); + if (process!=null && process.canTerminate()) { + Job job = new Job("Terminate process") { + protected IStatus run(IProgressMonitor monitor) { + try { + BootLaunchUtils.terminate(process.getLaunch()); + return Status.OK_STATUS; + } catch (Exception e) { + return ExceptionUtil.status(e); + } + } + + }; + job.schedule(); + } + } catch (Exception e) { + BootActivator.log(e); + } + } + } + + private ProcessConsole console; + private ProcessTracker processTracker; + private TerminateProcessAction terminateAction; + + public void activated() { + // ignore + } + + public void deactivated() { + // ignore + } + + public void dispose() { + if (processTracker!=null) { + processTracker.dispose(); + processTracker = null; + } + } + + public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) { + return null; + } + + public void init(IPageBookViewPage page, IConsole console) { + this.console = (ProcessConsole)console; + + //TODO: This code works assuming that our IConsolePageParticipant is called after the + // ProcessConsolePageParticipant (which creates the action we are replacing + //When testing this that was always the case... but it may not be guaranteed. + + if (isDevtoolsClient(this.console) || isBootApp(this.console)) { + terminateAction = new TerminateProcessAction(); + try { + terminateAction.setImageDescriptor(BootUIImages.descriptor("icons/stop.png")); + terminateAction.setDisabledImageDescriptor(BootUIImages.descriptor("icons/stop_disabled.png")); + } catch (Exception e) { + BootActivator.log(e); + } + IToolBarManager toolbar = page.getSite().getActionBars().getToolBarManager(); + IContributionItem replace = findReplacementItem(toolbar); + if (replace!=null) { + toolbar.appendToGroup(IConsoleConstants.LAUNCH_GROUP, terminateAction); + toolbar.remove(replace); + } + boolean enabled = getConsoleProcess().canTerminate(); + terminateAction.setEnabled(enabled); + if (enabled) { + this.processTracker = new ProcessTracker(new ProcessListenerAdapter() { + @Override + public void processTerminated(ProcessTracker tracker, IProcess terminated) { + if (getConsoleProcess().equals(terminated)) { + terminateAction.setEnabled(false); + //after process is terminated... it can't come back to life so... can stop listening now. + tracker.dispose(); + } + } + }); + } + } + } + + private boolean isDevtoolsClient(ProcessConsole console) { + return isLaunchType(console, BootDevtoolsClientLaunchConfigurationDelegate.TYPE_ID); + } + + private IProcess getConsoleProcess() { + if (console!=null) { + return console.getProcess(); + } + return null; + } + + private boolean isBootApp(ProcessConsole console) { + return isLaunchType(console, BootLaunchConfigurationDelegate.TYPE_ID); + } + + private IContributionItem findReplacementItem(IToolBarManager toolbar) { + SubContributionManager contributions = (SubContributionManager) toolbar; + for (IContributionItem item : contributions.getItems()) { + if (item instanceof ActionContributionItem) { + ActionContributionItem actionItem = (ActionContributionItem) item; + IAction replaceAction = actionItem.getAction(); + if (replaceAction.getClass().getName().equals("org.eclipse.debug.internal.ui.views.console.ConsoleTerminateAction")) { + return item; + } + } + } + return null; + } + + private static boolean isLaunchType(ProcessConsole console, String typeId) { + return isLaunchType(console.getProcess(), typeId); + } + + private static boolean isLaunchType(IProcess process, String launchTypeId) { + try { + if (process!=null) { + ILaunch launch = process.getLaunch(); + if (launch!=null) { + ILaunchConfiguration conf = launch.getLaunchConfiguration(); + if (conf!=null) { + return launchTypeId.equals(conf.getType().getIdentifier()); + } + } + } + } catch (Exception e) { + BootActivator.log(e); + } + return false; + } + + +} + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchConfigurationDelegate.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchConfigurationDelegate.java new file mode 100644 index 000000000..9ca113e0e --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchConfigurationDelegate.java @@ -0,0 +1,288 @@ +/******************************************************************************* + * Copyright (c) 2015, 2018 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.launch.devtools; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.debug.core.model.IProcess; +import org.eclipse.jdt.internal.launching.LaunchingMessages; +import org.eclipse.jdt.internal.launching.LaunchingPlugin; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jdt.launching.IVMConnector; +import org.eclipse.jdt.launching.JavaRuntime; +import org.eclipse.osgi.util.NLS; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.util.WaitFor; +import org.springframework.ide.eclipse.boot.util.ProcessListenerAdapter; +import org.springframework.ide.eclipse.boot.util.ProcessTracker; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +@SuppressWarnings("restriction") +public class BootDevtoolsClientLaunchConfigurationDelegate extends AbstractBootLaunchConfigurationDelegate { + + public static final String TYPE_ID = "org.springframework.ide.eclipse.boot.devtools.client.launch"; + + private static final long DEBUG_CONNECT_TIMEOUT = 20000; + public static final String REMOTE_SPRING_APPLICATION = "org.springframework.boot.devtools.RemoteSpringApplication"; + public static final String REMOTE_URL = "spring.devtools.remote.url"; + public static final String REMOTE_SECRET = "spring.devtools.remote.secret"; + public static final String DEFAULT_REMOTE_SECRET = ""; + public static final String DEBUG_PORT = "spring.devtools.remote.debug.local-port"; + + private static final String MANAGED = "spring.devtools.isManagedLaunch"; + + private final ThreadLocal localDebugPort = new ThreadLocal<>(); + + @Override + public String getMainTypeName(ILaunchConfiguration configuration) throws CoreException { + return REMOTE_SPRING_APPLICATION; + } + + @Override + public String getProgramArguments(ILaunchConfiguration conf) throws CoreException { + Properties props = getApplicationProperties(conf); + ArrayList args = new ArrayList<>(); + addPropertiesArguments(args, props); + String secret = getSecret(conf); + if (StringUtil.hasText(secret)) { + args.add(propertyAssignmentArgument(REMOTE_SECRET, secret)); + } + Integer debugPort = localDebugPort.get(); + if (debugPort!=null) { + args.add(propertyAssignmentArgument(DEBUG_PORT, ""+debugPort)); + } + args.add(getRemoteUrl(conf)); + return DebugPlugin.renderArguments(args.toArray(new String[args.size()]), null); + } + + private String getSecret(ILaunchConfiguration conf) { + try { + return conf.getAttribute(REMOTE_SECRET, DEFAULT_REMOTE_SECRET); + } catch (CoreException e) { + BootActivator.log(e); + } + return ""; + } + + @Override + public void launch(ILaunchConfiguration conf, String mode, ILaunch launch, IProgressMonitor mon) + throws CoreException { + boolean isDebug = ILaunchManager.DEBUG_MODE.equals(mode); + int work = isDebug ? 2 : 1; + mon.beginTask("Launching Devtools Client for"+getProjectName(conf), work); + if (isDebug) { + localDebugPort.set(findFreePort()); + } + try { + //Launch client: Generally we don't wanna debug the client itself so always use 'RUN_MODE' + + super.launch(conf, ILaunchManager.RUN_MODE, launch, new SubProgressMonitor(mon, 1)); + if (isDebug) { + //TODO: set debug port in config (or chosen dynamically?) + launchRemote(localDebugPort.get(), conf, launch, new SubProgressMonitor(mon, 1)); + } + } finally { + localDebugPort.remove(); + mon.done(); + } + } + + /** + * Returns a free port number on localhost, or -1 if unable to find a free port. + * + * @return a free port number on localhost, or -1 if unable to find a free port + */ + public static int findFreePort() { + try (ServerSocket socket = new ServerSocket(0)) { + socket.setReuseAddress(true); + return socket.getLocalPort(); + } catch (IOException e) { + } + return -1; + } + + + public static String getRemoteUrl(ILaunchConfiguration conf) { + try { + return conf.getAttribute(REMOTE_URL, (String)null); + } catch (CoreException e) { + BootActivator.log(e); + } + return null; + } + + public static void setRemoteUrl(ILaunchConfigurationWorkingCopy conf, String value) { + conf.setAttribute(REMOTE_URL, value); + } + + public static void setRemoteSecret(ILaunchConfigurationWorkingCopy conf, String value) { + conf.setAttribute(REMOTE_SECRET, value); + } + + public static String getRemoteSecret(ILaunchConfiguration conf) { + try { + return conf.getAttribute(REMOTE_SECRET, (String)null); + } catch (CoreException e) { + BootActivator.log(e); + } + return null; + } + + /** + * Create debugging target similar to a remote debugging session would and add them to the launch. + * This is to support debugging of the remote boot-app that is reachable over http tunnel + * the client creates. From our side this just as if we are opening a remote debug + * session to the client. + */ + private void launchRemote(int port, ILaunchConfiguration configuration, final ILaunch launch, IProgressMonitor _monitor) throws CoreException { + if (port<0) { + return; + } + final IProgressMonitor monitor = _monitor==null?new NullProgressMonitor():_monitor; + + monitor.beginTask(NLS.bind(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Attaching_to__0_____1, new String[]{configuration.getName()}), 3); + // check for cancellation + if (monitor.isCanceled()) { + return; + } + try { + monitor.subTask(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Verifying_launch_attributes____1); + + //String connectorId = "org.eclipse.jdt.launching.socketListenConnector";//getVMConnectorId(configuration); + String connectorId = "org.eclipse.jdt.launching.socketAttachConnector"; + final IVMConnector connector = JavaRuntime.getVMConnector(connectorId); + if (connector == null) { + abort(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Connector_not_specified_2, null, IJavaLaunchConfigurationConstants.ERR_CONNECTOR_NOT_AVAILABLE); + } + + final Map argMap = new HashMap<>(); + + int connectTimeout = Platform.getPreferencesService().getInt( + LaunchingPlugin.ID_PLUGIN, + JavaRuntime.PREF_CONNECT_TIMEOUT, + JavaRuntime.DEF_CONNECT_TIMEOUT, + null); + argMap.put("hostname", "localhost"); + argMap.put("timeout", ""+connectTimeout); + argMap.put("port", ""+port); + + // check for cancellation + if (monitor.isCanceled()) { + return; + } + + monitor.worked(1); + + //Don't think we need to set source location since the main launch method already does this. + +// monitor.subTask(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Creating_source_locator____2); +// // set the default source locator if required +// setDefaultSourceLocator(launch, configuration); +// monitor.worked(1); + + // connect to remote VM + try { + new WaitFor(DEBUG_CONNECT_TIMEOUT) { + public void run() throws Exception { + connector.connect(argMap, monitor, launch); + } + }; + new ProcessTracker(new ProcessListenerAdapter() { + public void debugTargetTerminated(ProcessTracker tracker, IDebugTarget target) { + handleTermination(tracker, target.getLaunch()); + } + public void processTerminated(ProcessTracker tracker, IProcess process) { + handleTermination(tracker, process.getLaunch()); + } + private void handleTermination(ProcessTracker tracker, ILaunch targetLaunch) { + if (launch.equals(targetLaunch)) { + tracker.dispose(); + terminateAllTargets(launch); + } + } + }); + } catch (Exception e) { + terminateAllTargets(launch); + throw ExceptionUtil.coreException(e); + } + + // check for cancellation + if (monitor.isCanceled()) { + terminateAllTargets(launch); + return; + } + } + finally { + monitor.done(); + } + } + + public void terminateAllTargets(final ILaunch launch) { + //Note: its better to discconect debugtargets before terminating processes + // because that allows a cleaner disconnect from the debugged process. + // (If the devtools client process is terminated its no longer possible to talk to the + // debugged process). + IDebugTarget[] debugTargets = launch.getDebugTargets(); + for (int i = 0; i < debugTargets.length; i++) { + IDebugTarget target = debugTargets[i]; + if (target.canDisconnect()) { + try { + target.disconnect(); + } catch (Exception e) { + BootActivator.log(e); + } + } + } + IProcess[] processes = launch.getProcesses(); + for (IProcess process : processes) { + if (process.canTerminate()) { + try { + process.terminate(); + } catch (Exception e) { + BootActivator.log(e); + } + } + } + } + + public static void setManaged(ILaunchConfigurationWorkingCopy wc, boolean isManaged) { + wc.setAttribute(MANAGED, isManaged); + } + + public static boolean isManaged(ILaunchConfiguration c) { + try { + return c.getAttribute(MANAGED, false); + } catch (CoreException e) { + Log.log(e); + return false; + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchConfigurationTabGroup.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchConfigurationTabGroup.java new file mode 100644 index 000000000..52df2c0a1 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchConfigurationTabGroup.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch.devtools; + +import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup; +import org.eclipse.debug.ui.CommonTab; +import org.eclipse.debug.ui.EnvironmentTab; +import org.eclipse.debug.ui.ILaunchConfigurationDialog; +import org.eclipse.debug.ui.ILaunchConfigurationTab; +import org.eclipse.debug.ui.ILaunchConfigurationTabGroup; +import org.eclipse.debug.ui.sourcelookup.SourceLookupTab; +import org.eclipse.jdt.debug.ui.launchConfigurations.JavaClasspathTab; +import org.eclipse.jdt.debug.ui.launchConfigurations.JavaJRETab; + +public class BootDevtoolsClientLaunchConfigurationTabGroup extends AbstractLaunchConfigurationTabGroup { + + /** + * @see ILaunchConfigurationTabGroup#createTabs(ILaunchConfigurationDialog, String) + */ + public void createTabs(ILaunchConfigurationDialog dialog, String mode) { + ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] { + new BootDevtoolsClientMainTab(), + //new JavaArgumentsTab(), // Removed client is launched with specific arguments set from main tab params + new JavaJRETab(), + new JavaClasspathTab(), + new SourceLookupTab(), + new EnvironmentTab(), + new CommonTab() + }; + setTabs(tabs); + } + +} \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchShortcut.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchShortcut.java new file mode 100644 index 000000000..5fb356b9d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchShortcut.java @@ -0,0 +1,210 @@ +/******************************************************************************* + * Copyright (c) 2012 GoPivotal, 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: + * GoPivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch.devtools; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.ui.DebugUITools; +import org.eclipse.debug.ui.IDebugModelPresentation; +import org.eclipse.debug.ui.IDebugUIConstants; +import org.eclipse.debug.ui.ILaunchShortcut; +import org.eclipse.jdt.internal.debug.ui.JDIDebugUIPlugin; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.dialogs.ElementListSelectionDialog; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; + +@SuppressWarnings("restriction") +public class BootDevtoolsClientLaunchShortcut implements ILaunchShortcut { + + @Override + public void launch(ISelection selection, String mode) { + try { + IResource rsrc = getResource(selection); + launch(rsrc, mode); + } catch (Throwable e) { + BootActivator.log(e); + } + } + + public void launch(IResource rsrc, String mode) throws CoreException { + if (rsrc!=null && rsrc.getType()==IResource.PROJECT) { + ILaunchConfiguration conf = findOrCreateConfiguration((IProject) rsrc); + if (conf!=null) { + if (isLaunchable(conf)) { + DebugUITools.launch(conf, mode); + } else { + IStructuredSelection selection = new StructuredSelection(new Object[] { conf }); + DebugUITools.openLaunchConfigurationDialogOnGroup(getShell(), selection, getLaunchGroup(mode)); + } + } + } + } + + /** + * Decide whether a launch conf is ready to launch as is or should be opened in + * launc conf editor to allow user to fill in more info. + */ + private boolean isLaunchable(ILaunchConfiguration conf) { + IProject project = BootLaunchConfigurationDelegate.getProject(conf); + String url = BootDevtoolsClientLaunchConfigurationDelegate.getRemoteUrl(conf); + return project!=null && + BootPropertyTester.isBootProject(project) && + BootPropertyTester.hasDevtools(project) && + StringUtil.hasText(url); + } + + private String getLaunchGroup(String launchMode) { + if (ILaunchManager.RUN_MODE.equals(launchMode)) { + return IDebugUIConstants.ID_RUN_LAUNCH_GROUP; + } else if (ILaunchManager.DEBUG_MODE.equals(launchMode)) { + return IDebugUIConstants.ID_DEBUG_LAUNCH_GROUP; + } else { + return IDebugUIConstants.ID_DEBUG_LAUNCH_GROUP; + } + } + + + private ILaunchConfiguration findOrCreateConfiguration(IProject project) throws CoreException { + List candidates = findConfigurations(project); + if (candidates.isEmpty()) { + return createConfiguration(project); + } else if (candidates.size()==1) { + return candidates.get(0); + } else { + return chooseConfiguration(project, candidates); + } + } + + private List findConfigurations(IProject project) throws CoreException { + ILaunchManager lm = getLaunchManager(); + List configs = new ArrayList<>(); + for (ILaunchConfiguration c : lm.getLaunchConfigurations(getLaunchType())) { + if (Objects.equals(BootLaunchConfigurationDelegate.getProject(c), project)) { + configs.add(c); + } + } + return configs; + } + + /** + * Returns a configuration from the given collection of configurations that should be launched, + * or null to cancel. Default implementation opens a selection dialog that allows + * the user to choose one of the specified launch configurations. Returns the chosen configuration, + * or null if the user cancels. + * + * @param configList list of configurations to choose from + * @return configuration to launch or null to cancel + */ + private ILaunchConfiguration chooseConfiguration(IProject project, List configList) { + IDebugModelPresentation labelProvider = DebugUITools.newDebugModelPresentation(); + ElementListSelectionDialog dialog= new ElementListSelectionDialog(getShell(), labelProvider); + dialog.setElements(configList.toArray()); + dialog.setTitle("Several Devtools Client Configs found for "+project.getName()); + dialog.setMessage("Select an existing configuration"); + dialog.setMultipleSelection(false); + int result = dialog.open(); + labelProvider.dispose(); + if (result == Window.OK) { + return (ILaunchConfiguration) dialog.getFirstResult(); + } + return null; + } + + /** + * Convenience method to return the active workbench window shell. + * + * @return active workbench window shell + */ + protected Shell getShell() { + return JDIDebugUIPlugin.getActiveWorkbenchShell(); + } + + private ILaunchConfigurationType getLaunchType() { + return getLaunchManager().getLaunchConfigurationType(BootDevtoolsClientLaunchConfigurationDelegate.TYPE_ID); + } + + /** + * Returns the singleton launch manager. + * + * @return launch manager + */ + private ILaunchManager getLaunchManager() { + return DebugPlugin.getDefault().getLaunchManager(); + } + + protected ILaunchConfiguration createConfiguration(IProject project) throws CoreException { + ILaunchConfigurationType configType = getConfigurationType(); + String projectName = project.getName(); + ILaunchConfigurationWorkingCopy wc = configType.newInstance(null, getLaunchManager().generateLaunchConfigurationName("devtools-client["+projectName+"]")); + setDefaults(wc, project); + wc.setMappedResources(new IResource[] {project}); + ILaunchConfiguration config = wc.doSave(); + return config; + } + + private void setDefaults(ILaunchConfigurationWorkingCopy wc, IProject project) { + BootLaunchConfigurationDelegate.setProject(wc, project); + } + + protected ILaunchConfigurationType getConfigurationType() { + return getLaunchManager().getLaunchConfigurationType(BootDevtoolsClientLaunchConfigurationDelegate.TYPE_ID); + } + + private IResource getResource(ISelection selection) { + if (selection instanceof IStructuredSelection) { + IStructuredSelection ss = (IStructuredSelection) selection; + Object el = ss.getFirstElement(); + if (el instanceof IResource) { + return (IResource) el; + } else if (el instanceof IAdaptable) { + //Warning older Eclipse API 'getAdapter() Object not IResource + Object o = ((IAdaptable) el).getAdapter(IResource.class); + return (IResource)o; + } + } + return null; + } + + + @Override + public void launch(IEditorPart editor, String mode) { + try { + IEditorInput input = editor.getEditorInput(); + Object rsrc = input.getAdapter(IResource.class); + if (rsrc!=null) { + launch((IResource)rsrc, mode); + } + } catch (Throwable e) { + BootActivator.log(e); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchUIModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchUIModel.java new file mode 100644 index 000000000..1a7c5d04a --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientLaunchUIModel.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch.devtools; + +import org.eclipse.core.resources.IProject; +import org.springframework.ide.eclipse.boot.launch.ExistingBootProjectSelectionValidator; +import org.springframework.ide.eclipse.boot.launch.SelectProjectLaunchTabModel; +import org.springsource.ide.eclipse.commons.livexp.core.CompositeValidator; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.StringFieldModel; +import org.springsource.ide.eclipse.commons.livexp.core.validators.UrlValidator; + +/** + * Model for the 'main type' selection widgetry on a launchconfiguration tab. + *

+ * Contains the 'logic' for the UI except for the widgets themselves. + * Can be unit tested without having to instantiate launch configuration dialogs + * etc. + * + * @author Kris De Volder + */ +public class BootDevtoolsClientLaunchUIModel { + + public final SelectProjectLaunchTabModel project; + public final StringFieldLaunchTabModel remoteUrl; + public final StringFieldLaunchTabModel remoteSecret; + + public BootDevtoolsClientLaunchUIModel() { + project = createProjectSelectionModel(); + remoteUrl = createRemoteUrlModel(); + remoteSecret = createRemoteSecretModel(); + } + + private StringFieldLaunchTabModel createRemoteUrlModel() { + StringFieldModel field = new StringFieldModel("Remote Url", ""); + field.validator( + new UrlValidator(field) + .allowedSchemes("http", "https") + ); + return new StringFieldLaunchTabModel(field, BootDevtoolsClientLaunchConfigurationDelegate.REMOTE_URL); + } + + private StringFieldLaunchTabModel createRemoteSecretModel() { + StringFieldModel field = new StringFieldModel("Remote Secret", ""); + return new StringFieldLaunchTabModel(field, BootDevtoolsClientLaunchConfigurationDelegate.REMOTE_SECRET); + } + + + private SelectProjectLaunchTabModel createProjectSelectionModel() { + LiveVariable project = new LiveVariable(); + CompositeValidator validator = new CompositeValidator(); + validator.addChild(new ExistingBootProjectSelectionValidator(project)); + validator.addChild(new DevtoolsEnabledValidator(project)); + return new SelectProjectLaunchTabModel(project, validator); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientMainTab.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientMainTab.java new file mode 100644 index 000000000..e440cde40 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/BootDevtoolsClientMainTab.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch.devtools; + +import static org.springframework.ide.eclipse.boot.ui.BootUIImages.BOOT_DEVTOOLS_ICON; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.swt.graphics.Image; +import org.springframework.ide.eclipse.boot.launch.SelectProjectLaunchTabSection; +import org.springframework.ide.eclipse.boot.launch.util.LaunchConfigurationTabWithSections; +import org.springframework.ide.eclipse.boot.ui.BootUIImages; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; + +/** + * @author Kris De Volder + */ +public class BootDevtoolsClientMainTab extends LaunchConfigurationTabWithSections implements IPageWithSections { + + @Override + public String getName() { + return "Devtools Client"; + } + + @Override + public Image getImage() { + return BootUIImages.getImage(BOOT_DEVTOOLS_ICON); + } + + @Override + protected List createSections() { + BootDevtoolsClientLaunchUIModel model = new BootDevtoolsClientLaunchUIModel(); + return Arrays.asList(new IPageSection[] { + SelectProjectLaunchTabSection.create(this, model.project), + StringFieldLaunchTabSection.create(this, model.remoteUrl), + StringFieldLaunchTabSection.create(this, model.remoteSecret), +// new MainTypeLaunchTabSection(this, model.project.selection).readonly(true), +// new ProfileLaunchTabSection(this, model.profile), +// new HLineSection(this), +// new EnableDebugSection(this, model.enableDebug), +// new EnableJmxSection(this, model.enableJmx), +// new HLineSection(this), +// new PropertiesTableSection(this, model.project.selection) + }); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/DevtoolsEnabledValidator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/DevtoolsEnabledValidator.java new file mode 100644 index 000000000..1eec6a32c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/DevtoolsEnabledValidator.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch.devtools; + +import org.eclipse.core.resources.IProject; +import org.springframework.ide.eclipse.boot.core.BootPropertyTester; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; + +public class DevtoolsEnabledValidator extends Validator { + + private LiveVariable project; + + public DevtoolsEnabledValidator(LiveVariable project) { + this.project = project; + dependsOn(project); + } + + @Override + protected ValidationResult compute() { + IProject p = project.getValue(); + if (p!=null) { + if (!BootPropertyTester.hasDevtools(p)) { + return ValidationResult.error("Project '"+p.getName()+"' does not have spring-boot-devtools on its classpath"); + } + } + return ValidationResult.OK; + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/StringFieldLaunchTabModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/StringFieldLaunchTabModel.java new file mode 100644 index 000000000..6bbaf8d62 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/StringFieldLaunchTabModel.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch.devtools; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.launch.LaunchTabSelectionModel; +import org.springsource.ide.eclipse.commons.livexp.core.StringFieldModel; + +public class StringFieldLaunchTabModel extends LaunchTabSelectionModel { + + public final StringFieldModel field; + public String attributeId; + + public StringFieldLaunchTabModel(StringFieldModel field, String attributeId) { + super(field.getVariable(), field.getValidator()); + this.field = field; + this.attributeId = attributeId; + } + + @Override + public void initializeFrom(ILaunchConfiguration conf) { + selection.setValue(getAttribute(conf)); + getDirtyState().setValue(false); + } + + @Override + public void performApply(ILaunchConfigurationWorkingCopy conf) { + setAttribute(conf, selection.getValue()); + getDirtyState().setValue(false); + } + + @Override + public void setDefaults(ILaunchConfigurationWorkingCopy conf) { + setAttribute(conf, getDefaultValue()); + } + + ///////////////////////////// + + private String getAttribute(ILaunchConfiguration conf) { + try { + return conf.getAttribute(attributeId, getDefaultValue()); + } catch (CoreException e) { + BootActivator.log(e); + } + return getDefaultValue(); + } + + private void setAttribute(ILaunchConfigurationWorkingCopy conf, String value) { + conf.setAttribute(attributeId, value); + } + + public String getDefaultValue() { + return field.getDefaultValue(); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/StringFieldLaunchTabSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/StringFieldLaunchTabSection.java new file mode 100644 index 000000000..b15bd289d --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/devtools/StringFieldLaunchTabSection.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2015 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.launch.devtools; + +import org.springframework.ide.eclipse.boot.launch.util.DelegatingLaunchConfigurationTabSection; +import org.springframework.ide.eclipse.boot.launch.util.LaunchConfigurationTabWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageSection; +import org.springsource.ide.eclipse.commons.livexp.ui.StringFieldSection; + +public class StringFieldLaunchTabSection { + + public static IPageSection create(LaunchConfigurationTabWithSections owner, StringFieldLaunchTabModel field) { + StringFieldSection ui = new StringFieldSection(owner, field.field); + return new DelegatingLaunchConfigurationTabSection(owner, field, ui); + } + + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/livebean/EnableJmxFeaturesModel.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/livebean/EnableJmxFeaturesModel.java new file mode 100644 index 000000000..1e6660930 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/livebean/EnableJmxFeaturesModel.java @@ -0,0 +1,201 @@ +/******************************************************************************* + * Copyright (c) 2015, 2016 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.launch.livebean; + +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.DEFAULT_ENABLE_JMX; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.DEFAULT_ENABLE_LIFE_CYCLE; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.DEFAULT_ENABLE_LIVE_BEAN_SUPPORT; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.DEFAULT_JMX_PORT; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.DEFAULT_TERMINATION_TIMEOUT; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.setEnableJMX; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.setEnableLifeCycle; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.setEnableLiveBeanSupport; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.setJMXPort; +import static org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.setTerminationTimeout; +import static org.springsource.ide.eclipse.commons.livexp.core.ValidationResult.error; + +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.osgi.framework.Bundle; +import org.springframework.ide.eclipse.boot.launch.BootLaunchActivator; +import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springframework.ide.eclipse.boot.launch.util.ILaunchConfigurationTabModel; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.Validator; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; + +/** + * Model for 'enable live bean support' widgetry on a launchconfiguration tab. + * + * @author Kris De Volder + */ +public class EnableJmxFeaturesModel implements ILaunchConfigurationTabModel { + + private static final int MAX_PORT = 65536; + + public final String portFieldName = "JMX Port"; + public final String timeOutFieldName = "Termination timeout"; + + public final LiveVariable jmxEnabled; + public final LiveVariable liveBeanEnabled; + public final LiveVariable lifeCycleEnabled; + + public final LiveVariable port; + public final LiveVariable terminationTimeout; + + private final Validator validator; + + private LiveVariable dirtyState = new LiveVariable<>(false); + + @SuppressWarnings("unchecked") + public EnableJmxFeaturesModel() { + this.jmxEnabled = new LiveVariable<>(DEFAULT_ENABLE_JMX); + this.liveBeanEnabled = new LiveVariable<>(DEFAULT_ENABLE_LIVE_BEAN_SUPPORT()); + this.lifeCycleEnabled = new LiveVariable<>(DEFAULT_ENABLE_LIFE_CYCLE); + + autoDisableWhenJmxDisabled(liveBeanEnabled, lifeCycleEnabled); + + this.port = new LiveVariable<>(""); + this.terminationTimeout = new LiveVariable<>(""); + jmxEnabled.addListener(makeDirty()); + liveBeanEnabled.addListener(makeDirty()); + lifeCycleEnabled.addListener(makeDirty()); + port.addListener(makeDirty()); + terminationTimeout.addListener(makeDirty()); + + this.validator = new Validator() { + { + dependsOn(jmxEnabled); + dependsOn(lifeCycleEnabled); + dependsOn(liveBeanEnabled); + dependsOn(terminationTimeout); + dependsOn(port); + } + + @Override + protected ValidationResult compute() { + boolean isEnabled = jmxEnabled.getValue(); + if (isEnabled) { + String portStr = port.getValue(); + if (!hasText(portStr)) { + return error(portFieldName+" must be specified"); + } + try { + int portValue = Integer.parseInt(portStr.trim()); + if (portValue<0) { + return error(portFieldName + " should be a positive integer or 0"); + } else if (portValue>MAX_PORT) { + return error(portFieldName + " should be smaller than "+MAX_PORT); + } + } catch (NumberFormatException e) { + return error(portFieldName+" can't be parsed as an Integer"); + } + } + if (lifeCycleEnabled.getValue()) { + String timeoutStr = terminationTimeout.getValue(); + if (!hasText(timeoutStr)) { + return error(timeOutFieldName+" must be specified"); + } + timeoutStr = timeoutStr.trim(); + try { + long timeout = Long.parseLong(timeoutStr); + if (!(timeout > 0)) { + return error(timeOutFieldName+" must be positive"); + } + } catch (NumberFormatException e) { + return error(timeOutFieldName+" can't be parsed as an Integer"); + } + } +// if (liveBeanEnabled.getValue() && !jmxEnabled.getValue()) { +// return error("Live Bean support requires JMX to be enabled"); +// } +// if (lifeCycleEnabled.getValue() && !jmxEnabled.getValue()) { +// return error("Lifecycle Management requires JMX to be enabled"); +// } + return ValidationResult.OK; + } + + }; + } + + private void autoDisableWhenJmxDisabled(@SuppressWarnings("unchecked") LiveVariable... featuresToDisable) { + jmxEnabled.addListener((exp, value) -> { + if (!value) { + for (LiveVariable feature : featuresToDisable) { + feature.setValue(false); + } + } + }); + } + + private boolean hasText(String portStr) { + return portStr!=null && !portStr.trim().equals(""); + } + + @SuppressWarnings("rawtypes") + protected ValueListener makeDirty() { + return new ValueListener() { + public void gotValue(LiveExpression exp, Object value) { + dirtyState.setValue(true); + } + }; + } + + @Override + public void initializeFrom(ILaunchConfiguration conf) { + jmxEnabled.setValue(BootLaunchConfigurationDelegate.getEnableJmx(conf)); + liveBeanEnabled.setValue(BootLaunchConfigurationDelegate.getEnableLiveBeanSupport(conf)); + lifeCycleEnabled.setValue(BootLaunchConfigurationDelegate.getEnableLifeCycle(conf)); + port.setValue(BootLaunchConfigurationDelegate.getJMXPort(conf)); + terminationTimeout.setValue(""+BootLaunchConfigurationDelegate.getTerminationTimeout(conf)); + getDirtyState().setValue(false); + } + + @Override + public void performApply(ILaunchConfigurationWorkingCopy conf) { + setEnableJMX(conf, jmxEnabled.getValue()); + setEnableLiveBeanSupport(conf, liveBeanEnabled.getValue()); + setEnableLifeCycle(conf, lifeCycleEnabled.getValue()); + setJMXPort(conf, StringUtil.trim(port.getValue())); + setTerminationTimeout(conf, StringUtil.trim(terminationTimeout.getValue())); + getDirtyState().setValue(false); + } + + @Override + public void setDefaults(ILaunchConfigurationWorkingCopy conf) { + setEnableLiveBeanSupport(conf, DEFAULT_ENABLE_LIVE_BEAN_SUPPORT()); + setEnableLifeCycle(conf, DEFAULT_ENABLE_LIFE_CYCLE); + setEnableJMX(conf, DEFAULT_ENABLE_JMX); + setJMXPort(conf, ""+DEFAULT_JMX_PORT); + setTerminationTimeout(conf, ""+DEFAULT_TERMINATION_TIMEOUT); + } + + @Override + public LiveVariable getDirtyState() { + return dirtyState; + } + + @Override + public LiveExpression getValidator() { + return validator; + } + + public boolean isLiveBeanSupported() { + BootLaunchActivator activator = BootLaunchActivator.getInstance(); + if (activator!=null) { + return activator.isLiveBeanSupported(); + } + return false; + } +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/livebean/EnableJmxSection.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/livebean/EnableJmxSection.java new file mode 100644 index 000000000..4cab27c93 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/livebean/EnableJmxSection.java @@ -0,0 +1,200 @@ +/******************************************************************************* + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch.livebean; + +import java.util.EnumSet; +import java.util.Optional; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.springframework.ide.eclipse.boot.launch.livebean.JmxBeanSupport.Feature; +import org.springframework.ide.eclipse.boot.launch.util.DelegatingLaunchConfigurationTabSection; +import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; +import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; +import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; +import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; +import org.springsource.ide.eclipse.commons.livexp.ui.UIConstants; +import org.springsource.ide.eclipse.commons.livexp.ui.WizardPageSection; + +/** + * LaunchTabSection that enables LiveBean graph support by adding required + * VM and program arguments. + * + * @author Kris De Volder + */ +public class EnableJmxSection extends DelegatingLaunchConfigurationTabSection { + + private static final boolean DEBUG = false;//(""+Platform.getLocation()).contains("kdvolder"); + + private static void debug(String string) { + if (DEBUG) { + System.out.println(string); + } + } + + static class UI extends WizardPageSection { + private Button jmxCheckbox; + private Optional