From a81a54f391f36369edd44b2724860bbd5429030c Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Wed, 11 Jul 2018 16:49:52 -0700 Subject: [PATCH] Partially working remote app live hover Uses a single-hard-coded jmx url to connect to remote and fetch actuator data. --- .../boot/app/cli/AbstractSpringBootApp.java | 493 ++++++++++++++++++ .../boot/app/cli/LocalSpringBootApp.java | 414 +-------------- .../boot/app/cli/NoArgumentsCacheHandler.java | 45 ++ .../boot/app/cli/RemoteSpringBootApp.java | 88 ++++ .../commons/boot/app/cli/SpringBootApp.java | 47 +- .../commons/util/FuctionWithException.java | 16 + .../java/handlers/BootJavaHoverProvider.java | 7 + .../java/handlers/RunningAppProvider.java | 30 +- .../boot/java/livehover/LiveHoverUtils.java | 7 +- .../java/utils/ChangeDetectionHistory.java | 28 +- .../vscode/boot/java/utils/ChangeHistory.java | 29 +- .../harness/MockRunningAppProvider.java | 2 +- 12 files changed, 741 insertions(+), 465 deletions(-) create mode 100644 headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java create mode 100644 headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/NoArgumentsCacheHandler.java create mode 100644 headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/RemoteSpringBootApp.java create mode 100644 headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FuctionWithException.java diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java new file mode 100644 index 000000000..af85db7c2 --- /dev/null +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java @@ -0,0 +1,493 @@ +/******************************************************************************* + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.boot.app.cli; + +import java.io.File; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.PlatformManagedObject; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.concurrent.TimeUnit; + +import javax.management.InstanceNotFoundException; +import javax.management.JMX; +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; +import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.Boot1xRequestMapping; +import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping; +import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMappingsParser20; +import org.springframework.ide.vscode.commons.util.FuctionWithException; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.StringUtil; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableList; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * A abstract base class which attempts to capture commonalities between + * Local and Remote connections to SpringBootApp using JMX. + */ +public abstract class AbstractSpringBootApp implements SpringBootApp { + + private static final String SPRINGFRAMEWORK_BOOT_DOMAIN = "org.springframework.boot"; + protected static Logger logger = LoggerFactory.getLogger(SpringBootApp.class); + + private String jmxMbeanActuatorDomain; + + // NOTE: Gson-based serialisation replaces the old Jackson ObjectMapper. Not sure if this makes a difference in the long run, but to retain the same output that Jackson Object Mapper + // was generating during serialisatino, some configuration in Gson is required, as the default behaviour of Gson is different than Object Mapper. + // Namely: Object Mapper does not escape Html, whereas Gson does by default (for example + // '=' in Gson appears as '\u003d') + protected final Gson gson = new GsonBuilder() + .disableHtmlEscaping() + .create(); + + protected abstract JMXServiceURL getJmxUrl() throws MalformedURLException; + + @Override + public abstract Properties getSystemProperties() throws Exception; + + @Override + public abstract String getProcessID(); + @Override + public abstract String getProcessName() throws Exception; + @Override + public abstract boolean isSpringBootApp(); + + @Override + public JMXConnector getJmxConnector() throws MalformedURLException, IOException { + JMXServiceURL serviceUrl = getJmxUrl(); + JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, null); + return jmxConnector; + } + + @Override + public final List getActiveProfiles() { + try { + String _env = getEnvironment(); + if (_env != null) { + JSONObject env = new JSONObject(_env); + Object _profiles = env.opt("activeProfiles"); //Boot 2.0 + if (_profiles==null) { + _profiles = env.opt("profiles"); //Boot 1.5 + } + if (_profiles instanceof JSONArray) { + JSONArray profiles = (JSONArray) _profiles; + ImmutableList.Builder list = ImmutableList.builder(); + for (Object object : profiles) { + if (object instanceof String) { + list.add((String) object); + } + } + return list.build(); + } + } + } catch (Exception e) { + logger.error("error resolving profiles from env", e); + } + return null; + } + + @Override + public Collection getRequestMappings() throws Exception { + //Boot 1.x + Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=requestMappingEndpoint"), "Data"); + if (result != null) { + String mappings = gson.toJson(result); + return parseRequestMappingsJson(mappings, "1.x"); + } + + //Boot 2.x + result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Mappings"), "mappings"); + if (result != null) { + String mappings = gson.toJson(result); + return parseRequestMappingsJson(mappings, "2.x"); + } + + return null; + } + + public static Collection parseRequestMappingsJson(String json, String bootVersion) { + JSONObject obj = new JSONObject(json); + if (bootVersion.equals("2.x")) { + return RequestMappingsParser20.parse(obj); + } else { //1.x + List result = new ArrayList<>(); + Iterator keys = obj.keys(); + while (keys.hasNext()) { + String rawKey = keys.next(); + JSONObject value = obj.getJSONObject(rawKey); + result.add(new Boot1xRequestMapping(rawKey, value)); + } + return result; + } + } + + protected ObjectName getObjectName(String keyProperties) throws Exception { + String domain = getDomainForActuator(); + return getObjectName(domain, keyProperties); + } + + protected ObjectName getObjectName(String domain, String keyProperties) throws Exception { + if (StringUtil.hasText(domain) && StringUtil.hasText(keyProperties)) { + String fullName = domain + ":" + keyProperties; + return ObjectName.getInstance(fullName); + } + return null; + } + + @Override + public final LiveBeansModel getBeans() { + try { + String domain = getDomainForActuator(); + String json = getBeansFromActuator(domain); + LiveBeansModel beans = LiveBeansModel.parse(json); + logger.info("Got {} beans for {}", beans.getBeanNames().size(), this); + return beans; + } catch (Exception e) { + logger.error("Error parsing beans", e); + return LiveBeansModel.builder().build(); + } + } + + private String getBeansFromActuator(String domain) throws Exception { + Object result = getActuatorDataFromAttribute(getObjectName(domain, "type=Endpoint,name=beansEndpoint"), "Data"); + if (result != null) { + String beans = gson.toJson(result); + return beans; + } + + result = getActuatorDataFromOperation(getObjectName(domain, "type=Endpoint,name=Beans"), "beans"); + if (result != null) { + String beans = gson.toJson(result); + return beans; + } + + return null; + } + + /** + * PT 156072399: Actuator information can be defined using a different JMX MBean domain. + * By default, Spring Boot exposes management endpoints as JMX MBeans under the 'org.springframework.boot' domain. + * Users can however define another domain in the app's application.properties, for example using this property: + * management.endpoints.jmx.domain=com.example.myapp + * + * Therefore we need to support other domains than just: 'org.springframework.boot' + * @return JMX MBean domain containing actuator information, or null if not resolved. + * @throws Exception when resolving domain from JMX + */ + protected final String getDomainForActuator() throws Exception { + if (this.jmxMbeanActuatorDomain == null) { + JMXConnector jmxConnector = null; + try { + jmxConnector = getJmxConnector(); + MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); + // To be more efficient in finding the domain containing actuator information, + // and avoid many JMX connections + // first check the default springframework boot domain: + String beansJson = getBeansFromActuator(SPRINGFRAMEWORK_BOOT_DOMAIN); + if (StringUtil.hasText(beansJson)) { + this.jmxMbeanActuatorDomain = SPRINGFRAMEWORK_BOOT_DOMAIN; + } + + if (this.jmxMbeanActuatorDomain == null) { + String[] domains = connection.getDomains(); + if (domains != null) { + for (String domain : domains) { + // we already checked default boot domain, no need to check it again + // Note that default spring boot domain may still appear even if another + // domain contains actuator Beans (for example, "Admin" will be under default spring framework domain) + if (!SPRINGFRAMEWORK_BOOT_DOMAIN.equals(domain)) { + beansJson = getBeansFromActuator(domain); + if (StringUtil.hasText(beansJson)) { + this.jmxMbeanActuatorDomain = domain; + break; + } + } + } + } + } + } finally { + if (jmxConnector != null) { + jmxConnector.close(); + } + } + } + return this.jmxMbeanActuatorDomain; + } + + protected R withPlatformMxBean(Class mbeanType, FuctionWithException doit) throws Exception { + JMXConnector jmxConnector = null; + try { + jmxConnector = getJmxConnector(); + MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); + T proxy = ManagementFactory.getPlatformMXBean(connection, mbeanType); + return doit.apply(proxy); + } finally { + if (jmxConnector != null) jmxConnector.close(); + } + } + + protected final Object getActuatorDataFromAttribute(ObjectName objectName, String attribute) throws Exception { + JMXConnector jmxConnector = null; + try { + if (objectName != null) { + jmxConnector = getJmxConnector(); + MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); + + try { + Object result = connection.getAttribute(objectName, "Data"); + return result; + } + catch (InstanceNotFoundException e) { + } + } + return null; + } + finally { + if (jmxConnector != null) jmxConnector.close(); + } + } + + protected final Object getActuatorDataFromOperation(ObjectName objectName, String operation) throws Exception { + JMXConnector jmxConnector = null; + try { + if (objectName != null) { + jmxConnector = getJmxConnector(); + MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); + + try { + Object result = connection.invoke(objectName, operation, null, null); + return result; + } + catch (InstanceNotFoundException e) { + } + } + return null; + } + finally { + if (jmxConnector != null) jmxConnector.close(); + } + } + + @Override + public String getEnvironment() throws Exception { + Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=environmentEndpoint"), "Data"); + if (result != null) { + String environment = gson.toJson(result); + return environment; + } + + result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Env"), "environment"); + if (result != null) { + String environment = gson.toJson(result); + return environment; + } + + return null; + } + + @Override + public String[] getClasspath() throws Exception { + Properties props = getSystemProperties(); + String classpath = (String) props.get("java.class.path"); + String[] cpElements = splitClasspath(classpath); + return cpElements; + } + + private String[] splitClasspath(String classpath) { + List classpathElements = new ArrayList<>(); + if (classpath != null) { + StringTokenizer tokenizer = new StringTokenizer(classpath, File.pathSeparator); + while (tokenizer.hasMoreTokens()) { + String classpathElement = tokenizer.nextToken(); + classpathElements.add(classpathElement); + } + } + return classpathElements.toArray(new String[classpathElements.size()]); + } + + @Override + public String getJavaCommand() throws Exception { + Properties props = getSystemProperties(); + System.err.println(">>>>> sysprops"); + for (Entry e : props.entrySet()) { + System.err.println(e.getKey() +"="+e.getValue()); + } + System.err.println("<<<< sysprops"); + return (String) props.get("sun.java.command"); + } + + @Override + public String getHost() throws Exception { + //TODO: different implementation for cf apps with locally tunnelled + // jmx connection? + JMXServiceURL serviceUrl = getJmxUrl(); + return serviceUrl.getHost(); + } + + @Override + public Optional> getLiveConditionals() throws Exception { + return getLiveConditionals(getAutoConfigReport(), getProcessID(), getProcessName()); + } + + /** + * Publicly visible so that it can be tested via a mock app + * + * @param autoConfigReport + * @param processId + * @param processName + * @return + * @throws Exception + */ + public static Optional> getLiveConditionals(String autoConfigReport, String processId, + String processName) { + return LiveConditionalParser.parse(autoConfigReport, processId, processName); + } + + private String getAutoConfigReport() throws Exception { + //Boot 1.x + Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=autoConfigurationReportEndpoint"), "Data"); + if (result != null) { + String report = gson.toJson(result); + return report; + } + + //Boot 2.x + result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Conditions"), "applicationConditionEvaluation"); + if (result != null) { + String report = gson.toJson(result); + return report; + } + + return null; + } + + @Override + public String getPort() throws Exception { + JMXConnector jmxConnector = null; + try { + jmxConnector = getJmxConnector(); + return getPort(jmxConnector); + } + finally { + if (jmxConnector != null) jmxConnector.close(); + } + } + + protected String getPort(JMXConnector jmxConnector) throws Exception { + MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); + + String port = getPortViaAdmin(connection); + if (port != null) { + return port; + } + + port = getPortViaActuator(connection); + if (port != null) { + return port; + } + + port = getPortViaTomcatBean(connection); + return port; + } + + protected String getPortViaAdmin(MBeanServerConnection connection) throws Exception { + try { + String DEFAULT_OBJECT_NAME = "org.springframework.boot:type=Admin,name=SpringApplication"; + ObjectName objectName = new ObjectName(DEFAULT_OBJECT_NAME); + + Object o = connection.invoke(objectName,"getProperty", new String[] {"local.server.port"}, new String[] {String.class.getName()}); + return o.toString(); + } + catch (InstanceNotFoundException e) { + return null; + } + } + + protected String getPortViaActuator(MBeanServerConnection connection) throws Exception { + String environment = getEnvironment(); + if (environment != null) { + JSONObject env = new JSONObject(environment); + if (env != null) { + JSONObject portsObject = env.optJSONObject("server.ports"); + if (portsObject != null) { + String portValue = portsObject.optString("local.server.port"); + if (portValue!=null) { + return portValue; + } + } + //Not found as direct property value... in Boot 2.0 we must look inside the 'propertySources'. + //Similar... but structure is more complex. + JSONArray propertySources = env.optJSONArray("propertySources"); + if (propertySources!=null) { + for (Object _source : propertySources) { + if (_source instanceof JSONObject) { + JSONObject source = (JSONObject) _source; + String sourceName = source.optString("name"); + if ("server.ports".equals(sourceName)) { + JSONObject props = source.optJSONObject("properties"); + JSONObject valueObject = props.optJSONObject("local.server.port"); + if (valueObject!=null) { + String portValue = valueObject.optString("value"); + if (portValue!=null) { + return portValue; + } + } + } + } + } + } + } + } + return null; + } + + protected 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")) { + Object result = connection.getAttribute(objectName, "localPort"); + if (result != null) { + return result.toString(); + } + } + } + } + catch (InstanceNotFoundException e) { + } + return null; + } + +} diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootApp.java index e340f2101..dd868a4b9 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootApp.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootApp.java @@ -15,7 +15,6 @@ import java.io.IOException; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.Optional; @@ -35,17 +34,10 @@ import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; -import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.Boot1xRequestMapping; -import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping; -import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMappingsParser20; import org.springframework.ide.vscode.commons.util.CollectorUtil; -import org.springframework.ide.vscode.commons.util.StringUtil; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; -import com.google.common.collect.ImmutableList; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; @@ -53,11 +45,9 @@ import com.sun.tools.attach.VirtualMachineDescriptor; /** * @author Martin Lippert */ -public class LocalSpringBootApp extends SpringBootApp { +public class LocalSpringBootApp extends AbstractSpringBootApp { - private static final String SPRINGFRAMEWORK_BOOT_DOMAIN = "org.springframework.boot"; - - private Logger logger = LoggerFactory.getLogger(LocalSpringBootApp.class); + private static final Logger logger = LoggerFactory.getLogger(LocalSpringBootApp.class); private VirtualMachine vm; private VirtualMachineDescriptor vmd; @@ -65,16 +55,6 @@ public class LocalSpringBootApp extends SpringBootApp { private static final String LOCAL_CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress"; private Boolean isSpringBootApp; - private String jmxMbeanActuatorDomain; - - // NOTE: Gson-based serialisation replaces the old Jackson ObjectMapper. Not sure if this makes a difference in the long run, but to retain the same output that Jackson Object Mapper - // was generating during serialisatino, some configuration in Gson is required, as the default behaviour of Gson is different than Object Mapper. - // Namely: Object Mapper does not escape Html, whereas Gson does by default (for example - // '=' in Gson appears as '\u003d') - private Gson gson = new GsonBuilder() - .disableHtmlEscaping() - .create(); - private final Supplier jmxConnect = Suppliers.memoize(() -> { String address = null; @@ -122,12 +102,6 @@ public class LocalSpringBootApp extends SpringBootApp { return vmd.displayName(); } - @Override - public String getHost() throws Exception { - JMXServiceURL serviceUrl = new JMXServiceURL(jmxConnect.get()); - return serviceUrl.getHost(); - } - @Override public boolean isSpringBootApp() { if (isSpringBootApp==null) { @@ -148,207 +122,27 @@ public class LocalSpringBootApp extends SpringBootApp { } private boolean isSpringBootAppSysprops() throws IOException { - Properties sysprops = this.vm.getSystemProperties(); + Properties sysprops = getSystemProperties(); return "org.springframework.boot.loader".equals(sysprops.getProperty("java.protocol.handler.pkgs")); } - private boolean isSpringBootAppClasspath() throws IOException { + private boolean isSpringBootAppClasspath() throws Exception { return contains(getClasspath(), "spring-boot"); } @Override - public String[] getClasspath() throws IOException { - Properties props = this.vm.getSystemProperties(); - String classpath = (String) props.get("java.class.path"); - String[] cpElements = splitClasspath(classpath); - return cpElements; - } - - @Override - public String getJavaCommand() throws IOException { - Properties props = this.vm.getSystemProperties(); - return (String) props.get("sun.java.command"); + public Properties getSystemProperties() throws IOException { + return this.vm.getSystemProperties(); } public boolean containsSystemProperty(Object key) throws IOException { - Properties props = this.vm.getSystemProperties(); + Properties props = getSystemProperties(); return props.containsKey(key); } @Override - public String getPort() throws Exception { - JMXConnector jmxConnector = null; - try { - jmxConnector = getJmxConnector(); - return getPort(jmxConnector); - } - finally { - if (jmxConnector != null) jmxConnector.close(); - } - } - - @Override - public String getEnvironment() throws Exception { - Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=environmentEndpoint"), "Data"); - if (result != null) { - String environment = gson.toJson(result); - return environment; - } - - result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Env"), "environment"); - if (result != null) { - String environment = gson.toJson(result); - return environment; - } - - return null; - } - - private String getBeansFromActuator(String domain) throws Exception { - Object result = getActuatorDataFromAttribute(getObjectName(domain, "type=Endpoint,name=beansEndpoint"), "Data"); - if (result != null) { - String beans = gson.toJson(result); - return beans; - } - - result = getActuatorDataFromOperation(getObjectName(domain, "type=Endpoint,name=Beans"), "beans"); - if (result != null) { - String beans = gson.toJson(result); - return beans; - } - - return null; - } - - @Override - public LiveBeansModel getBeans() { - try { - String domain = getDomainForActuator(); - String json = getBeansFromActuator(domain); - return LiveBeansModel.parse(json); - } catch (Exception e) { - logger.error("Error parsing beans", e); - return LiveBeansModel.builder().build(); - } - } - - public static Collection parseRequestMappingsJson(String json, String bootVersion) { - JSONObject obj = new JSONObject(json); - if (bootVersion.equals("2.x")) { - return RequestMappingsParser20.parse(obj); - } else { //1.x - List result = new ArrayList<>(); - Iterator keys = obj.keys(); - while (keys.hasNext()) { - String rawKey = keys.next(); - JSONObject value = obj.getJSONObject(rawKey); - result.add(new Boot1xRequestMapping(rawKey, value)); - } - return result; - } - } - - @Override - public Collection getRequestMappings() throws Exception { - //Boot 1.x - Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=requestMappingEndpoint"), "Data"); - if (result != null) { - String mappings = gson.toJson(result); - return parseRequestMappingsJson(mappings, "1.x"); - } - - //Boot 2.x - result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Mappings"), "mappings"); - if (result != null) { - String mappings = gson.toJson(result); - return parseRequestMappingsJson(mappings, "2.x"); - } - - return null; - } - - @Override - public Optional> getLiveConditionals() throws Exception { - return getLiveConditionals(getAutoConfigReport(), getProcessID(), getProcessName()); - } - - /** - * Publicly visible so that it can be tested via a mock app - * - * @param autoConfigReport - * @param processId - * @param processName - * @return - * @throws Exception - */ - public static Optional> getLiveConditionals(String autoConfigReport, String processId, - String processName) { - return LiveConditionalParser.parse(autoConfigReport, processId, processName); - } - - private String getAutoConfigReport() throws Exception { - //Boot 1.x - Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=autoConfigurationReportEndpoint"), "Data"); - if (result != null) { - String report = gson.toJson(result); - return report; - } - - //Boot 2.x - result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Conditions"), "applicationConditionEvaluation"); - if (result != null) { - String report = gson.toJson(result); - return report; - } - - return null; - } - - protected Object getActuatorDataFromAttribute(ObjectName objectName, String attribute) throws Exception { - JMXConnector jmxConnector = null; - try { - if (objectName != null) { - jmxConnector = getJmxConnector(); - MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); - - try { - Object result = connection.getAttribute(objectName, "Data"); - return result; - } - catch (InstanceNotFoundException e) { - } - } - return null; - } - finally { - if (jmxConnector != null) jmxConnector.close(); - } - } - - protected Object getActuatorDataFromOperation(ObjectName objectName, String operation) throws Exception { - JMXConnector jmxConnector = null; - try { - if (objectName != null) { - jmxConnector = getJmxConnector(); - MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); - - try { - Object result = connection.invoke(objectName, operation, null, null); - return result; - } - catch (InstanceNotFoundException e) { - } - } - return null; - } - finally { - if (jmxConnector != null) jmxConnector.close(); - } - } - protected JMXConnector getJmxConnector() throws MalformedURLException, IOException { - JMXServiceURL serviceUrl = new JMXServiceURL(jmxConnect.get()); - JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, null); - return jmxConnector; + protected JMXServiceURL getJmxUrl() throws MalformedURLException { + return new JMXServiceURL(jmxConnect.get()); } protected boolean contains(String[] cpElements, String element) { @@ -360,170 +154,9 @@ public class LocalSpringBootApp extends SpringBootApp { return false; } - protected String[] splitClasspath(String classpath) { - List classpathElements = new ArrayList<>(); - if (classpath != null) { - StringTokenizer tokenizer = new StringTokenizer(classpath, File.pathSeparator); - while (tokenizer.hasMoreTokens()) { - String classpathElement = tokenizer.nextToken(); - classpathElements.add(classpathElement); - } - } - return classpathElements.toArray(new String[classpathElements.size()]); - } - - protected String getPort(JMXConnector jmxConnector) throws Exception { - MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); - - String port = getPortViaAdmin(connection); - if (port != null) { - return port; - } - - port = getPortViaActuator(connection); - if (port != null) { - return port; - } - - port = getPortViaTomcatBean(connection); - return port; - } - - protected String getPortViaAdmin(MBeanServerConnection connection) throws Exception { - try { - String DEFAULT_OBJECT_NAME = "org.springframework.boot:type=Admin,name=SpringApplication"; - ObjectName objectName = new ObjectName(DEFAULT_OBJECT_NAME); - - Object o = connection.invoke(objectName,"getProperty", new String[] {"local.server.port"}, new String[] {String.class.getName()}); - return o.toString(); - } - catch (InstanceNotFoundException e) { - return null; - } - } - - protected String getPortViaActuator(MBeanServerConnection connection) throws Exception { - String environment = getEnvironment(); - if (environment != null) { - JSONObject env = new JSONObject(environment); - if (env != null) { - JSONObject portsObject = env.optJSONObject("server.ports"); - if (portsObject != null) { - String portValue = portsObject.optString("local.server.port"); - if (portValue!=null) { - return portValue; - } - } - //Not found as direct property value... in Boot 2.0 we must look inside the 'propertySources'. - //Similar... but structure is more complex. - JSONArray propertySources = env.optJSONArray("propertySources"); - if (propertySources!=null) { - for (Object _source : propertySources) { - if (_source instanceof JSONObject) { - JSONObject source = (JSONObject) _source; - String sourceName = source.optString("name"); - if ("server.ports".equals(sourceName)) { - JSONObject props = source.optJSONObject("properties"); - JSONObject valueObject = props.optJSONObject("local.server.port"); - if (valueObject!=null) { - String portValue = valueObject.optString("value"); - if (portValue!=null) { - return portValue; - } - } - } - } - } - } - } - } - return null; - } - - protected 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")) { - Object result = connection.getAttribute(objectName, "localPort"); - if (result != null) { - return result.toString(); - } - } - } - } - catch (InstanceNotFoundException e) { - } - return null; - } - - protected ObjectName getObjectName(String keyProperties) throws Exception { - String domain = getDomainForActuator(); - return getObjectName(domain, keyProperties); - } - - protected ObjectName getObjectName(String domain, String keyProperties) throws Exception { - if (StringUtil.hasText(domain) && StringUtil.hasText(keyProperties)) { - String fullName = domain + ":" + keyProperties; - return ObjectName.getInstance(fullName); - } - return null; - } - - /** - * PT 156072399: Actuator information can be defined using a different JMX MBean domain. - * By default, Spring Boot exposes management endpoints as JMX MBeans under the 'org.springframework.boot' domain. - * Users can however define another domain in the app's application.properties, for example using this property: - * management.endpoints.jmx.domain=com.example.myapp - * - * Therefore we need to support other domains than just: 'org.springframework.boot' - * @return JMX MBean domain containing actuator information, or null if not resolved. - * @throws Exception when resolving domain from JMX - */ - protected String getDomainForActuator() throws Exception { - if (this.jmxMbeanActuatorDomain == null) { - JMXConnector jmxConnector = null; - try { - jmxConnector = getJmxConnector(); - MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); - // To be more efficient in finding the domain containing actuator information, - // and avoid many JMX connections - // first check the default springframework boot domain: - String beansJson = getBeansFromActuator(SPRINGFRAMEWORK_BOOT_DOMAIN); - if (StringUtil.hasText(beansJson)) { - this.jmxMbeanActuatorDomain = SPRINGFRAMEWORK_BOOT_DOMAIN; - } - - if (this.jmxMbeanActuatorDomain == null) { - String[] domains = connection.getDomains(); - if (domains != null) { - for (String domain : domains) { - // we already checked default boot domain, no need to check it again - // Note that default spring boot domain may still appear even if another - // domain contains actuator Beans (for example, "Admin" will be under default spring framework domain) - if (!SPRINGFRAMEWORK_BOOT_DOMAIN.equals(domain)) { - beansJson = getBeansFromActuator(domain); - if (StringUtil.hasText(beansJson)) { - this.jmxMbeanActuatorDomain = domain; - break; - } - } - } - } - } - } finally { - if (jmxConnector != null) { - jmxConnector.close(); - } - } - } - return this.jmxMbeanActuatorDomain; - } - @Override public String toString() { - return "Process [id=" +getProcessID() + ", name=`"+getProcessName()+"`]"; + return "LocalSpringBootApp [id=" +getProcessID() + ", name=`"+getProcessName()+"`]"; } /** @@ -547,33 +180,6 @@ public class LocalSpringBootApp extends SpringBootApp { System.out.println("}"); } - @Override - public List getActiveProfiles() { - try { - String _env = getEnvironment(); - if (_env != null) { - JSONObject env = new JSONObject(_env); - Object _profiles = env.opt("activeProfiles"); //Boot 2.0 - if (_profiles==null) { - _profiles = env.opt("profiles"); //Boot 1.5 - } - if (_profiles instanceof JSONArray) { - JSONArray profiles = (JSONArray) _profiles; - ImmutableList.Builder list = ImmutableList.builder(); - for (Object object : profiles) { - if (object instanceof String) { - list.add((String) object); - } - } - return list.build(); - } - } - } catch (Exception e) { - logger.error("error resolving profiles from env", e); - } - return null; - } - public void dispose() { if (vm!=null) { logger.info("SpringBootApp disposed: "+this); diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/NoArgumentsCacheHandler.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/NoArgumentsCacheHandler.java new file mode 100644 index 000000000..af61b0858 --- /dev/null +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/NoArgumentsCacheHandler.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.boot.app.cli; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +public class NoArgumentsCacheHandler implements InvocationHandler { + + private Cache cache; + private Object delegate; + + public NoArgumentsCacheHandler(Object delegate, Duration cacheDuration) { + this.delegate = delegate; + this.cache = CacheBuilder.newBuilder().expireAfterWrite(cacheDuration.toMillis(), TimeUnit.MILLISECONDS).build(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (args==null || args.length==0) { + return invokeCached(proxy, method); + } + return method.invoke(proxy, args); + } + + private Object invokeCached(Object proxy, Method method) throws Exception { + return cache.get(method.getName(), () -> { + return method.invoke(delegate, new Object[] {}); + }); + } + +} diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/RemoteSpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/RemoteSpringBootApp.java new file mode 100644 index 000000000..ad72fb19e --- /dev/null +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/RemoteSpringBootApp.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.boot.app.cli; + +import java.io.IOException; +import java.lang.management.RuntimeMXBean; +import java.lang.reflect.Proxy; +import java.net.MalformedURLException; +import java.time.Duration; +import java.util.Map.Entry; +import java.util.Properties; + +import javax.management.remote.JMXServiceURL; + +public class RemoteSpringBootApp extends AbstractSpringBootApp { + + private String jmxUrl; + + private RemoteSpringBootApp(String jmxUrl) { + this.jmxUrl = jmxUrl; + } + + @Override + protected JMXServiceURL getJmxUrl() throws MalformedURLException { + return new JMXServiceURL(jmxUrl); + } + + @Override + public Properties getSystemProperties() throws Exception { + return withPlatformMxBean(RuntimeMXBean.class, runtime -> { + Properties props = new Properties(); + for (Entry e : runtime.getSystemProperties().entrySet()) { + props.put(e.getKey(), e.getValue()); + } + return props; + }); + } + + @Override + public boolean isSpringBootApp() { + //For now, let's assume that, if its not a boot app, then we won't create + //a RemoteSpringBootApp instance for it. + return true; + } + + @Override + public String getProcessID() { + try { + return withPlatformMxBean(RuntimeMXBean.class, runtime -> runtime.getName()); + } catch (Exception e) { + return "Unknown-PID"; + } + } + + @Override + public String getProcessName() throws Exception { + try { + String command = getJavaCommand(); + if (command!=null) { + int space = command.indexOf(' '); + if (space>=0) { + command = command.substring(0, space); + } + command = command.trim(); + if (!"".equals(command)) { + return command; + } + } + } catch (IOException e) { + logger.equals(e); + } + return "Unknown"; + } + + public static SpringBootApp create(String jmxUrl) { + RemoteSpringBootApp delegate = new RemoteSpringBootApp(jmxUrl); + return (SpringBootApp) Proxy.newProxyInstance(RemoteSpringBootApp.class.getClassLoader(), new Class[] {SpringBootApp.class}, new NoArgumentsCacheHandler(delegate, Duration.ofMillis(4900))); + } + +} diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java index e1199002e..c5dba0f4b 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java @@ -1,42 +1,33 @@ -/******************************************************************************* - * 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 - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Pivotal, Inc. - initial API and implementation - *******************************************************************************/ package org.springframework.ide.vscode.commons.boot.app.cli; import java.io.IOException; +import java.net.MalformedURLException; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.Properties; + +import javax.management.remote.JMXConnector; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping; -/** - * A abstract base class which attempts to capture commonalities between - * Local and Remote connections to SpringBootApp using JMX. - */ -public abstract class SpringBootApp { +public interface SpringBootApp { - public abstract String getProcessID(); - public abstract String getProcessName(); - public abstract boolean isSpringBootApp(); - public abstract String[] getClasspath() throws IOException; - public abstract String getJavaCommand() throws IOException; - public abstract String getHost() throws Exception; - public abstract String getPort() throws Exception; - - public abstract Optional> getLiveConditionals() throws Exception; - public abstract Collection getRequestMappings() throws Exception; - public abstract LiveBeansModel getBeans(); - public abstract List getActiveProfiles(); - public abstract String getEnvironment() throws Exception; + String[] getClasspath() throws Exception; + String getJavaCommand() throws Exception; + String getProcessName() throws Exception; + String getProcessID(); + String getHost() throws Exception; + String getPort() throws Exception; + boolean isSpringBootApp(); + String getEnvironment() throws Exception; + Collection getRequestMappings() throws Exception; + LiveBeansModel getBeans(); + List getActiveProfiles(); + Optional> getLiveConditionals() throws Exception; + Properties getSystemProperties() throws Exception; + JMXConnector getJmxConnector() throws MalformedURLException, IOException; } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FuctionWithException.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FuctionWithException.java new file mode 100644 index 000000000..ba59e5373 --- /dev/null +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FuctionWithException.java @@ -0,0 +1,16 @@ +/******************************************************************************* + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.util; + +@FunctionalInterface +public interface FuctionWithException { + O apply(I in) throws Exception; +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java index 300c8a587..9318ace1c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java @@ -29,6 +29,8 @@ import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentPositionParams; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareLookup; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; @@ -51,6 +53,8 @@ import com.google.common.collect.ImmutableList; */ public class BootJavaHoverProvider implements HoverHandler { + private static Logger logger = LoggerFactory.getLogger(BootJavaHoverProvider.class); + private JavaProjectFinder projectFinder; private BootJavaLanguageServerComponents server; private AnnotationHierarchyAwareLookup hoverProviders; @@ -220,14 +224,17 @@ public class BootJavaHoverProvider implements HoverHandler { private Hover provideHoverForAnnotation(ASTNode exactNode, Annotation annotation, int offset, TextDocument doc, IJavaProject project) { ITypeBinding type = annotation.resolveTypeBinding(); if (type != null) { + logger.info("Hover requested for "+type.getName()); SpringBootApp[] runningApps = getRunningSpringApps(project); if (runningApps.length > 0) { for (HoverProvider provider : this.hoverProviders.get(type)) { Hover hover = provider.provideHover(exactNode, annotation, type, offset, doc, project, runningApps); if (hover!=null) { + logger.info("Hover found: "+hover); //TODO: compose multiple hovers somehow instead of just returning the first one? return hover; } + logger.info("NO Hover!"); } //Only reaching here if we didn't get a hover. if (!hasActuatorDependency(project)) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/RunningAppProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/RunningAppProvider.java index 53fd20ad1..116212fb9 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/RunningAppProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/RunningAppProvider.java @@ -14,14 +14,40 @@ import java.util.Collection; import org.springframework.ide.vscode.commons.boot.app.cli.LocalSpringBootApp; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; +import org.springframework.ide.vscode.commons.boot.app.cli.RemoteSpringBootApp; import com.google.common.collect.ImmutableList; public interface RunningAppProvider { - public static final RunningAppProvider LOCAL_APPS = LocalSpringBootApp::getAllRunningSpringApps; + public static final SpringBootApp HARDCODED_REMOTE_APP = //TODO: proper configuration of remote apps instead of this hard-coded stuff which is just for testing. + RemoteSpringBootApp.create("service:jmx:rmi://localhost:9111/jndi/rmi://localhost:9111/jmxrmi"); + + static RunningAppProvider composite(RunningAppProvider... children) { + if (children.length==1) { + return children[0]; + } + return () -> { + ImmutableList.Builder allApps = ImmutableList.builder(); + for (RunningAppProvider c : children) { + Collection moreApps = c.getAllRunningSpringApps(); + if (moreApps!=null) { + allApps.addAll(moreApps); + } + } + return allApps.build(); + }; + } + + public static final RunningAppProvider LOCAL_APPS = LocalSpringBootApp::getAllRunningSpringApps; + public static final RunningAppProvider REMOTE_APPS = () -> ImmutableList.of( + HARDCODED_REMOTE_APP + ); + public static final RunningAppProvider DEFAULT = composite( + LOCAL_APPS +// REMOTE_APPS + ); - public static final RunningAppProvider DEFAULT = LocalSpringBootApp::getAllRunningSpringApps; public static final RunningAppProvider NULL = () -> ImmutableList.of(); Collection getAllRunningSpringApps() throws Exception; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java index 3c618227e..0e3cc892c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java @@ -87,7 +87,12 @@ public class LiveHoverUtils { } public static String niceAppName(SpringBootApp app) { - return niceAppName(app.getProcessID(), app.getProcessName()); + try { + return niceAppName(app.getProcessID(), app.getProcessName()); + } catch (Exception e) { + e.printStackTrace(); + return app.toString(); + } } public static String niceAppName(String processId, String processName) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ChangeDetectionHistory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ChangeDetectionHistory.java index ca3109e1a..67cf3ce61 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ChangeDetectionHistory.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ChangeDetectionHistory.java @@ -23,25 +23,25 @@ import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; * @author Martin Lippert */ public class ChangeDetectionHistory { - + private Map changeHistory; - + public ChangeDetectionHistory() { this.changeHistory = new HashMap<>(); } public Change[] checkForChanges(SpringBootApp[] runningApps) { List result = null; - + for (SpringBootApp runningApp : runningApps) { String virtualID = getVirtualAppID(runningApp); - + if (changeHistory.containsKey(virtualID)) { // the standard case ChangeHistory appHistory = changeHistory.get(virtualID); appHistory.updateProcess(runningApp); - Change change = appHistory.checkForUpdates(); + Change change = appHistory.checkForUpdates(); if (change != null) { if (result == null) { result = new ArrayList<>(); @@ -55,8 +55,8 @@ public class ChangeDetectionHistory { ChangeHistory oldHistory = changeHistory.remove(oldAppID); oldHistory.updateProcess(runningApp); changeHistory.put(virtualID, oldHistory); - - Change change = oldHistory.checkForUpdates(); + + Change change = oldHistory.checkForUpdates(); if (change != null) { if (result == null) { result = new ArrayList<>(); @@ -68,12 +68,12 @@ public class ChangeDetectionHistory { ChangeHistory newHistory = new ChangeHistory(); newHistory.updateProcess(runningApp); changeHistory.put(virtualID, newHistory); - + newHistory.checkForUpdates(); } } } - + if (result != null) { return (Change[]) result.toArray(new Change[result.size()]); } @@ -85,10 +85,10 @@ public class ChangeDetectionHistory { private String getVirtualAppID(SpringBootApp app) { return app.getProcessID(); } - + private String getOldApp(SpringBootApp app, SpringBootApp[] allApps) { Set histories = this.changeHistory.keySet(); - + try { String commandLine = app.getJavaCommand(); String[] classpath = app.getClasspath(); @@ -100,10 +100,10 @@ public class ChangeDetectionHistory { } } } - catch (IOException e) { + catch (Exception e) { e.printStackTrace(); } - + return null; } @@ -119,7 +119,7 @@ public class ChangeDetectionHistory { catch (Exception e) { e.printStackTrace(); } - + return true; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ChangeHistory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ChangeHistory.java index ff20f9234..e159786ad 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ChangeHistory.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ChangeHistory.java @@ -10,7 +10,6 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.utils; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -37,21 +36,21 @@ public class ChangeHistory { public ChangeHistory() { } - + public void updateProcess(SpringBootApp app) { if (this.associatedProcess != app) { this.associatedProcess = app; - + try { this.associatedProcessCommand = app.getJavaCommand(); this.associatedProcessClasspath = app.getClasspath(); } - catch (IOException e) { + catch (Exception e) { e.printStackTrace(); } } } - + public boolean matchesProcess(String commandLine, String[] classpath) { return this.associatedProcessCommand != null && this.associatedProcessCommand.equals(commandLine) && this.associatedProcessClasspath != null && Arrays.deepEquals(this.associatedProcessClasspath, classpath); @@ -71,7 +70,7 @@ public class ChangeHistory { lastBeans = currentBeans; } } - + return result; } @@ -79,20 +78,20 @@ public class ChangeHistory { if (previous == current) { return result; } - + Set currentNames = current.getBeanNames(); Set previousNames = previous.getBeanNames(); - + Set allNames = new HashSet<>(currentNames); allNames.addAll(previousNames); - + for (String name : allNames) { List currentBeans = current.getBeansOfName(name); List previousBeans = previous.getBeansOfName(name); - + result = calculateBeansDiff(previousBeans, currentBeans, result); } - + return result; } @@ -110,10 +109,10 @@ public class ChangeHistory { result.addDeletedBean(bean); } } - + for (LiveBean bean : currentBeans) { if (!contains(previousBeans, bean)) { - + if (result == null) { result = new Change(associatedProcess); } @@ -121,7 +120,7 @@ public class ChangeHistory { result.addNewBean(bean); } } - + return result; } @@ -133,7 +132,7 @@ public class ChangeHistory { return true; } } - + return false; } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java index 69a93d89c..7d5ec9281 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java @@ -85,7 +85,7 @@ public class MockRunningAppProvider { return this; } - public MockAppBuilder processName(String name) { + public MockAppBuilder processName(String name) throws Exception { this.processName = name; when(app.getProcessName()).thenReturn(name); return this;