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 new file mode 100644 index 000000000..e340f2101 --- /dev/null +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootApp.java @@ -0,0 +1,590 @@ +/******************************************************************************* + * Copyright (c) 2017, 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.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 javax.management.InstanceNotFoundException; +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.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; + +/** + * @author Martin Lippert + */ +public class LocalSpringBootApp extends SpringBootApp { + + private static final String SPRINGFRAMEWORK_BOOT_DOMAIN = "org.springframework.boot"; + + private Logger logger = LoggerFactory.getLogger(LocalSpringBootApp.class); + + private VirtualMachine vm; + private VirtualMachineDescriptor vmd; + + 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; + try { + address = vm.getAgentProperties().getProperty(LOCAL_CONNECTOR_ADDRESS); + } catch (Exception e) { + //ignore + } + if (address==null) { + try { + address = vm.startLocalManagementAgent(); + } catch (IOException e) { + logger.error("Error starting local management agent", e); + } + } + logger.info("Java PID = "+getProcessID()+" jmx address = "+address); + return address; + }); + + private static LocalSpringBootAppCache cache = new LocalSpringBootAppCache(); + + public static Collection getAllRunningJavaApps() throws Exception { + return cache.getAllRunningJavaApps(); + } + /** + * @return Map that contains the boot apps, mapping the process ID -> boot app accessor object + */ + public static Collection getAllRunningSpringApps() throws Exception { + return getAllRunningJavaApps().stream().filter(SpringBootApp::isSpringBootApp).collect(CollectorUtil.toImmutableList()); + } + + public LocalSpringBootApp(VirtualMachineDescriptor vmd) throws AttachNotSupportedException, IOException { + this.vmd = vmd; + this.vm = VirtualMachine.attach(vmd); + logger.info("SpringBootApp created: "+this); + } + + @Override + public String getProcessID() { + return vmd.id(); + } + + @Override + public String getProcessName() { + 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) { + try { + isSpringBootApp = !containsSystemProperty("sts4.languageserver.name") + && ( + isSpringBootAppClasspath() || + isSpringBootAppSysprops() + ); + } catch (Exception e) { + //Couldn't determine if the VM is a spring boot app. Could be it already died. Or could be its not accessible (yet). + // We will ignore the exception, pretend its not a boot app (most likely isn't) but DO NOT CACHE this result + // so it will be retried again on the next polling loop. + return false; + } + } + return isSpringBootApp; + } + + private boolean isSpringBootAppSysprops() throws IOException { + Properties sysprops = this.vm.getSystemProperties(); + return "org.springframework.boot.loader".equals(sysprops.getProperty("java.protocol.handler.pkgs")); + } + + private boolean isSpringBootAppClasspath() throws IOException { + 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 boolean containsSystemProperty(Object key) throws IOException { + Properties props = this.vm.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 boolean contains(String[] cpElements, String element) { + for (String cpElement : cpElements) { + if (cpElement.contains(element)) { + return true; + } + } + 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()+"`]"; + } + + /** + * For testing / investigation purposes. Dumps out as much information as possible + * that can be onbtained from the jvm, without accessing JMX. + */ + public void dumpJvmInfo() throws IOException { + System.out.println("--- vm infos ----"); + System.out.println("id = "+vm.id()); + System.out.println("displayName = "+vmd.displayName()); + dump("agentProperties", vm.getAgentProperties()); + dump("systemProps", vm.getSystemProperties()); + System.out.println("-----------------"); + } + + private void dump(String name, Properties props) { + System.out.println(name + " = {"); + for (Entry prop : props.entrySet()) { + System.out.println(" "+prop.getKey()+" = "+prop.getValue()); + } + 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); + try { + vm.detach(); + } catch (Exception e) { + } + vm = null; + } + if (vmd!=null) { + vmd = null; + } + } +} diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppCache.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootAppCache.java similarity index 78% rename from headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppCache.java rename to headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootAppCache.java index b5506bce1..884ca6d8a 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppCache.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootAppCache.java @@ -15,34 +15,35 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; -public class SpringBootAppCache { +public class LocalSpringBootAppCache { private static final Duration EXPIRE_AFTER = Duration.ofMillis(500); //Limits rate at which we refresh list of apps private long nextRefreshAfter = Long.MIN_VALUE; - private ImmutableMap apps = ImmutableMap.of(); + private ImmutableMap apps = ImmutableMap.of(); public synchronized Collection getAllRunningJavaApps() { if (System.currentTimeMillis()>=nextRefreshAfter) { refresh(); } - return apps.values(); + return ImmutableList.copyOf(apps.values()); } private void refresh() { List currentVms = VirtualMachine.list(); - ImmutableMap.Builder newAppsBuilder = ImmutableMap.builder(); + ImmutableMap.Builder newAppsBuilder = ImmutableMap.builder(); for (VirtualMachineDescriptor vm : currentVms) { - SpringBootApp existingApp = apps.get(vm); + LocalSpringBootApp existingApp = apps.get(vm); if (existingApp!=null) { newAppsBuilder.put(vm, existingApp); } else { try { - newAppsBuilder.put(vm, new SpringBootApp(vm)); + newAppsBuilder.put(vm, new LocalSpringBootApp(vm)); } catch (Exception e) { //Ignore problems attaching to a VM. We will try again on next polling loop, if vm still exists. //The most likely cause is that the VM already died since we obtained a reference to it. @@ -50,7 +51,7 @@ public class SpringBootAppCache { } } HashSet oldVms = new HashSet<>(apps.keySet()); - ImmutableMap newApps = newAppsBuilder.build(); + ImmutableMap newApps = newAppsBuilder.build(); oldVms.removeAll(newApps.keySet()); for (VirtualMachineDescriptor oldVm : oldVms) { apps.get(oldVm).dispose(); 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 e6c494c13..e1199002e 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,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2018 Pivotal, Inc. + * 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 @@ -10,570 +10,33 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.boot.app.cli; -import java.io.File; 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; -import java.util.Properties; -import java.util.Set; -import java.util.StringTokenizer; -import javax.management.InstanceNotFoundException; -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.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; /** - * @author Martin Lippert + * A abstract base class which attempts to capture commonalities between + * Local and Remote connections to SpringBootApp using JMX. */ -public class SpringBootApp { +public abstract class SpringBootApp { - private static final String SPRINGFRAMEWORK_BOOT_DOMAIN = "org.springframework.boot"; + 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; - private Logger logger = LoggerFactory.getLogger(SpringBootApp.class); - - private VirtualMachine vm; - private VirtualMachineDescriptor vmd; - - 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; - try { - address = vm.getAgentProperties().getProperty(LOCAL_CONNECTOR_ADDRESS); - } catch (Exception e) { - //ignore - } - if (address==null) { - try { - address = vm.startLocalManagementAgent(); - } catch (IOException e) { - logger.error("Error starting local management agent", e); - } - } - return address; - }); - - private static SpringBootAppCache cache = new SpringBootAppCache(); - - public static Collection getAllRunningJavaApps() throws Exception { - return cache.getAllRunningJavaApps(); - } - /** - * @return Map that contains the boot apps, mapping the process ID -> boot app accessor object - */ - public static Collection getAllRunningSpringApps() throws Exception { - return getAllRunningJavaApps().stream().filter(SpringBootApp::isSpringBootApp).collect(CollectorUtil.toImmutableList()); - } - - public SpringBootApp(VirtualMachineDescriptor vmd) throws AttachNotSupportedException, IOException { - this.vmd = vmd; - this.vm = VirtualMachine.attach(vmd); - logger.info("SpringBootApp created: "+this); - } - - public String getProcessID() { - return vmd.id(); - } - - public String getProcessName() { - return vmd.displayName(); - } - - public String getHost() throws Exception { - JMXServiceURL serviceUrl = new JMXServiceURL(jmxConnect.get()); - return serviceUrl.getHost(); - } - - public boolean isSpringBootApp() { - if (isSpringBootApp==null) { - try { - isSpringBootApp = !containsSystemProperty("sts4.languageserver.name") - && ( - isSpringBootAppClasspath() || - isSpringBootAppSysprops() - ); - } catch (Exception e) { - //Couldn't determine if the VM is a spring boot app. Could be it already died. Or could be its not accessible (yet). - // We will ignore the exception, pretend its not a boot app (most likely isn't) but DO NOT CACHE this result - // so it will be retried again on the next polling loop. - return false; - } - } - return isSpringBootApp; - } - - private boolean isSpringBootAppSysprops() throws IOException { - Properties sysprops = this.vm.getSystemProperties(); - return "org.springframework.boot.loader".equals(sysprops.getProperty("java.protocol.handler.pkgs")); - } - - private boolean isSpringBootAppClasspath() throws IOException { - return contains(getClasspath(), "spring-boot"); - } - - public String[] getClasspath() throws IOException { - Properties props = this.vm.getSystemProperties(); - String classpath = (String) props.get("java.class.path"); - String[] cpElements = splitClasspath(classpath); - return cpElements; - } - - public String getJavaCommand() throws IOException { - Properties props = this.vm.getSystemProperties(); - return (String) props.get("sun.java.command"); - } - - public boolean containsSystemProperty(Object key) throws IOException { - Properties props = this.vm.getSystemProperties(); - return props.containsKey(key); - } - - public String getPort() throws Exception { - JMXConnector jmxConnector = null; - try { - jmxConnector = getJmxConnector(); - return getPort(jmxConnector); - } - finally { - if (jmxConnector != null) jmxConnector.close(); - } - } - - 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; - } - - 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; - } - } - - 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 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 boolean contains(String[] cpElements, String element) { - for (String cpElement : cpElements) { - if (cpElement.contains(element)) { - return true; - } - } - 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()+"`]"; - } - - /** - * For testing / investigation purposes. Dumps out as much information as possible - * that can be onbtained from the jvm, without accessing JMX. - */ - public void dumpJvmInfo() throws IOException { - System.out.println("--- vm infos ----"); - System.out.println("id = "+vm.id()); - System.out.println("displayName = "+vmd.displayName()); - dump("agentProperties", vm.getAgentProperties()); - dump("systemProps", vm.getSystemProperties()); - System.out.println("-----------------"); - } - - private void dump(String name, Properties props) { - System.out.println(name + " = {"); - for (Entry prop : props.entrySet()) { - System.out.println(" "+prop.getKey()+" = "+prop.getValue()); - } - System.out.println("}"); - } - - 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); - try { - vm.detach(); - } catch (Exception e) { - } - vm = null; - } - if (vmd!=null) { - vmd = null; - } - } + 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; } diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppCLI.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppCLI.java index acfaebd27..681bc424a 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppCLI.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppCLI.java @@ -18,7 +18,7 @@ import java.util.Collection; public class SpringBootAppCLI { public static void main(String[] args) throws Exception { - Collection allRunningJavaApps = SpringBootApp.getAllRunningJavaApps(); + Collection allRunningJavaApps = LocalSpringBootApp.getAllRunningJavaApps(); for (SpringBootApp app : allRunningJavaApps) { if (app.isSpringBootApp()) { printBootAppDetails(app); diff --git a/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppTest.java b/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootAppTest.java similarity index 80% rename from headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppTest.java rename to headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootAppTest.java index ee3b0ce17..f911612d7 100644 --- a/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppTest.java +++ b/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootAppTest.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.json.JSONObject; @@ -39,7 +40,7 @@ import org.springframework.ide.vscode.commons.util.test.ACondition; import com.google.common.collect.ImmutableList; -public class SpringBootAppTest { +public class LocalSpringBootAppTest { private static final String[] appNames = { "actuator-client-15-test-subject", // Boot 1.5 test app @@ -57,7 +58,7 @@ public class SpringBootAppTest { public static void setupClass() throws Exception { testAppRunners = Arrays.asList(appNames).stream().map(appName -> { try { - return startTestApplication(SpringBootAppTest.class.getResource("/boot-apps/"+appName+"-0.0.1-SNAPSHOT.jar")); + return startTestApplication(LocalSpringBootAppTest.class.getResource("/boot-apps/"+appName+"-0.0.1-SNAPSHOT.jar")); } catch (Exception e) { throw ExceptionUtil.unchecked(e); } @@ -88,11 +89,13 @@ public class SpringBootAppTest { ); } - private SpringBootApp getAppContaining(String nameFragment) { + private LocalSpringBootApp getAppContaining(String nameFragment) { try { - Collection allApps = SpringBootApp.getAllRunningJavaApps(); + Collection allApps = LocalSpringBootApp.getAllRunningJavaApps(); try { - return allApps.stream().filter(app -> app.getProcessName().contains(nameFragment)).findAny().get(); + SpringBootApp result = allApps.stream().filter(localAppWithNameContaining(nameFragment)) + .findAny().get(); + return (LocalSpringBootApp) result; } catch (NoSuchElementException e) { //Improve error message for debugging... StringBuilder foundAppNames = new StringBuilder(); @@ -107,35 +110,44 @@ public class SpringBootAppTest { } } + private Predicate localAppWithNameContaining(String nameFragment) { + return app -> { + if (app instanceof LocalSpringBootApp) { + return ((LocalSpringBootApp)app).getProcessName().contains(nameFragment); + } + return false; + }; + } + @Ignore @Test public void dumpJvmInfo() throws Exception { //Ignored because this test may have timing issues. Still useful to // run locally and inspect dump results, but may need some tweaking. for (String appName : appNames) { - SpringBootApp testApp = getAppContaining(appName); + LocalSpringBootApp testApp = getAppContaining(appName); testApp.dumpJvmInfo(); System.out.println("======================================"); } } @Test public void getAllJavaApps() throws Exception { - Collection allApps = SpringBootApp.getAllRunningJavaApps(); + Collection allApps = LocalSpringBootApp.getAllRunningJavaApps(); for (String appName : appNames) { - Optional myProcess = allApps.stream().filter(app -> app.getProcessName().contains(appName)).findAny(); + Optional myProcess = allApps.stream().filter(localAppWithNameContaining(appName)).findAny(); assertTrue(appName, myProcess.isPresent()); } } @Test public void getAllBootApps() throws Exception { - Collection allApps = SpringBootApp.getAllRunningSpringApps(); + Collection allApps = LocalSpringBootApp.getAllRunningSpringApps(); for (String appName : appNames) { - Optional myProcess = allApps.stream().filter(app -> app.getProcessName().contains(appName)).findAny(); + Optional myProcess = allApps.stream().filter(localAppWithNameContaining(appName)).findAny(); assertTrue(myProcess.isPresent()); } } @Test public void getPort() throws Exception { - for (SpringBootApp testApp : getTestApps()) { + for (LocalSpringBootApp testApp : getTestApps()) { ACondition.waitFor(TIMEOUT, () -> { int port = Integer.parseInt(testApp.getPort()); assertTrue(port > 0); @@ -144,7 +156,7 @@ public class SpringBootAppTest { } } - private Collection getTestApps() throws Exception { + private Collection getTestApps() throws Exception { return ACondition.waitForValue(TIMEOUT, () -> Arrays.asList(appNames).stream() .map(this::getAppContaining) .collect(Collectors.toList()) @@ -153,7 +165,7 @@ public class SpringBootAppTest { @Test public void getHost() throws Exception { - for (SpringBootApp testApp : getTestApps()) { + for (LocalSpringBootApp testApp : getTestApps()) { System.err.println("getHost for "+testApp); ACondition.waitFor(TIMEOUT, () -> { String host = testApp.getHost(); @@ -165,7 +177,7 @@ public class SpringBootAppTest { @Test public void getEnvironment() throws Exception { - for (SpringBootApp testApp : getTestApps()) { + for (LocalSpringBootApp testApp : getTestApps()) { ACondition.waitFor(TIMEOUT, () -> { String env = testApp.getEnvironment(); assertNonEmptyJsonObject(env); @@ -175,7 +187,7 @@ public class SpringBootAppTest { @Test public void getBeans() throws Exception { - for (SpringBootApp testApp : getTestApps()) { + for (LocalSpringBootApp testApp : getTestApps()) { try { ACondition.waitFor(TIMEOUT, () -> { LiveBeansModel beansModel = testApp.getBeans(); @@ -191,7 +203,7 @@ public class SpringBootAppTest { @Test public void getRequestMappings() throws Exception { - for (SpringBootApp testApp : getTestApps()) { + for (LocalSpringBootApp testApp : getTestApps()) { try { ACondition.waitFor(TIMEOUT, () -> { Collection result = testApp.getRequestMappings(); @@ -206,7 +218,7 @@ public class SpringBootAppTest { @Test public void getLiveConditionals() throws Exception { - for (SpringBootApp testApp : getTestApps()) { + for (LocalSpringBootApp testApp : getTestApps()) { try { ACondition.waitFor(TIMEOUT, () -> { Optional> result = testApp.getLiveConditionals(); @@ -221,7 +233,7 @@ public class SpringBootAppTest { @Test public void getProfiles() throws Exception { - for (SpringBootApp testApp : getTestApps()) { + for (LocalSpringBootApp testApp : getTestApps()) { ACondition.waitFor(TIMEOUT, () -> { List result = testApp.getActiveProfiles(); assertEquals(ImmutableList.copyOf(TEST_PROFILES), result); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java index f2e598251..20775004b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java @@ -28,6 +28,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; import org.springframework.ide.vscode.boot.java.livehover.LiveHoverUtils; import org.springframework.ide.vscode.commons.boot.app.cli.LiveConditional; +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.java.IJavaProject; import org.springframework.ide.vscode.commons.util.Log; 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 7ccf04f77..53fd20ad1 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 @@ -12,13 +12,16 @@ package org.springframework.ide.vscode.boot.java.handlers; 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 com.google.common.collect.ImmutableList; public interface RunningAppProvider { - public static final RunningAppProvider DEFAULT = SpringBootApp::getAllRunningSpringApps; + public static final RunningAppProvider LOCAL_APPS = LocalSpringBootApp::getAllRunningSpringApps; + + 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/AbstractInjectedIntoHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java index 9324b714a..59c746984 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java @@ -36,9 +36,9 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.collect.ImmutableList; public abstract class AbstractInjectedIntoHoverProvider implements HoverProvider { - + protected BootJavaLanguageServerComponents server; - + public AbstractInjectedIntoHoverProvider(BootJavaLanguageServerComponents server) { this.server = server; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ActiveProfilesProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ActiveProfilesProvider.java index 9a6c89abe..a4f674ac3 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ActiveProfilesProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ActiveProfilesProvider.java @@ -57,13 +57,13 @@ public class ActiveProfilesProvider implements HoverProvider { for (SpringBootApp app : runningApps) { List profiles = app.getActiveProfiles(); if (profiles==null) { - markdown.append(niceAppName(app)+" : _Unknown_\n\n"); + markdown.append(LiveHoverUtils.niceAppName(app)+" : _Unknown_\n\n"); } else { hasInterestingApp = true; if (profiles.isEmpty()) { - markdown.append(niceAppName(app)+" : _None_\n\n"); + markdown.append(LiveHoverUtils.niceAppName(app)+" : _None_\n\n"); } else { - markdown.append(niceAppName(app)+" :\n"); + markdown.append(LiveHoverUtils.niceAppName(app)+" :\n"); for (String profile : profiles) { markdown.append("- "+profile+"\n"); } @@ -80,10 +80,6 @@ public class ActiveProfilesProvider implements HoverProvider { return null; } - private String niceAppName(SpringBootApp app) { - return "Process [PID="+app.getProcessID()+", name=`"+app.getProcessName()+"`]"; - } - @Override public Collection getLiveHoverHints(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { if (runningApps.length > 0) { 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 f74aa671f..3c618227e 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 @@ -26,7 +26,7 @@ import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.StringUtil; public class LiveHoverUtils { - + public static String showBean(LiveBean bean) { StringBuilder buf = new StringBuilder("Bean [id: " + bean.getId()); String type = bean.getType(true); @@ -69,14 +69,6 @@ public class LiveHoverUtils { return new SpringResource(sourceLinks, resource, project).toMarkdown(); } - public static String niceAppName(SpringBootApp app) { - return niceAppName(app.getProcessID() ,app.getProcessName()); - } - - public static String niceAppName(String processId, String processName) { - return "Process [PID=" + processId + ", name=`" + processName + "`]"; - } - public static boolean hasRelevantBeans(SpringBootApp app, LiveBean definedBean) { return findRelevantBeans(app, definedBean).findAny().isPresent(); } @@ -94,4 +86,13 @@ public class LiveHoverUtils { return Stream.empty(); } + public static String niceAppName(SpringBootApp app) { + return niceAppName(app.getProcessID(), app.getProcessName()); + } + + public static String niceAppName(String processId, String processName) { + return "Process [PID="+processId+", name=`"+processName+"`]"; + } + + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java index f0e1efba4..5e4031f05 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java @@ -12,6 +12,7 @@ package org.springframework.ide.vscode.boot.java.requestmapping; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.stream.Stream; @@ -22,6 +23,7 @@ import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.SymbolKind; import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; +import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping; import org.springframework.ide.vscode.commons.util.Log; /** 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 ca181f0f0..69a93d89c 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 @@ -18,6 +18,7 @@ import java.util.Collection; import org.mockito.Mockito; import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider; +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.livebean.LiveBeansModel; import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping; @@ -106,13 +107,13 @@ public class MockRunningAppProvider { } public MockAppBuilder requestMappings(String mappings) throws Exception { - Collection requestMappings = SpringBootApp.parseRequestMappingsJson(mappings, "1.x"); + Collection requestMappings = LocalSpringBootApp.parseRequestMappingsJson(mappings, "1.x"); when(app.getRequestMappings()).thenReturn(requestMappings); return this; } public MockAppBuilder liveConditionalsJson(String rawJson) throws Exception{ - when(app.getLiveConditionals()).thenReturn(SpringBootApp.getLiveConditionals(rawJson, processId, processName)); + when(app.getLiveConditionals()).thenReturn(LocalSpringBootApp.getLiveConditionals(rawJson, processId, processName)); return this; }